I pass a Path
argument to a function:
from pathlib import Path
def my_function(my_path: Path):
pass
and I would like to make the argument optional.
My first naive attempt doesn't work because Path()
(somewhat surprisingly to me) creates a PosixPath
object for the current directory (on macOS here; WindowsPath
on Windows):
from pathlib import Path
def my_function(my_path: Path = Path()):
if my_path:
print(f'my_path arg: {my_path}')
print(f'my_path type: {type(my_path)}')
else:
print(f'no my_path argument; falling back to some default (or so)')
my_function()
output:
my_path arg: .
my_path type: <class 'pathlib.PosixPath'>
The probably obvious approach to use None
does work:
from pathlib import Path
def my_function(my_path: Path = None):
if my_path:
print(f'my_path arg: {my_path}')
print(f'my_path type: {type(my_path)}')
else:
print(f'no my_path argument; falling back to some default (or so)')
my_function()
output:
no my_path argument; falling back to some default (or so)
but that causes a pyright
issue
Expression of type "None" cannot be assigned to parameter of type "Path"
Type "None" cannot be assigned to type "Path"
which forces me to consider Path
and None
types; I think that's what typing.Union
is for:
from pathlib import Path
from typing import Union
def my_function(my_path: Union[None, Path] = None):
if my_path:
print(f'my_path arg: {my_path}')
print(f'my_path type: {type(my_path)}')
else:
print(f'no my_path argument; falling back to some default (or so)')
my_function()
output:
no my_path argument; falling back to some default (or so)
THE PROBLEM: I have a lot of functions I pass Path
arguments to - and having to keep None
and Path
types in mind seems rather inelegant to me - and also causes all sorts of complications down the line. Therefore, I would really like something like a NonePath
that is of type Path
and of which I can create objects that evaluate to False
.
I found this SO answer (and learned about creating new types in python on the way 🙂) and figured from Truth Value Testing that in order to make an object testable for truthiness, I need to give it a __bool__()
method.
My next (still naive) attempt:
from pathlib import Path
NonePath = type('NonePath', (), {'__bool__': lambda: False})
def my_function(my_path: Path = NonePath):
if my_path:
print(f'my_path arg: {my_path}')
print(f'my_path type: {type(my_path)}')
else:
print(f'no my_path argument; falling back to some default (or so)')
my_function()
output:
my_path arg: <class '__main__.NonePath'>
my_path type: <class 'type'>
is not correct of course - I need to use a NonePath()
object as default, not a NonePath
type; next attempt:
from pathlib import Path
NonePath = type('NonePath', (), {'__bool__': lambda: False})
def my_function(my_path: Path = NonePath()): # <-- adding ()
if my_path:
print(f'my_path arg: {my_path}')
print(f'my_path type: {type(my_path)}')
else:
print(f'no my_path argument; falling back to some default (or so)')
my_function()
output:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in my_function
TypeError: <lambda>() takes 0 positional arguments but 1 was given
I don't really understand the error and continuing to screw around cluelessly with this hasn't gotten me anywhere - but simply returning False
(or should it be None
?) anyway seems not the right approach: Shouldn't I return True
/ False
based on some test on one of the object's attributes ? Which ones ?
Also, shouldn't I use Path
as the base for NonePath
? Didn't really get anywhere with that, either:
from pathlib import Path
# NOTE: trailing comma required to make second arg a tuple
NonePath = type('NonePath', (Path,), {'__bool__': lambda: False})
def my_function(my_path: Path = NonePath()):
if my_path:
print(f'my_path arg: {my_path}')
print(f'my_path type: {type(my_path)}')
else:
print(f'no my_path argument; falling back to some default (or so)')
output:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/Cellar/python@3.10/3.10.12_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/pathlib.py", line 960, in __new__
self = cls._from_parts(args)
File "/usr/local/Cellar/python@3.10/3.10.12_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/pathlib.py", line 594, in _from_parts
drv, root, parts = self._parse_args(args)
File "/usr/local/Cellar/python@3.10/3.10.12_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/pathlib.py", line 587, in _parse_args
return cls._flavour.parse_parts(parts)
AttributeError: type object 'NonePath' has no attribute '_flavour'
Sounds to me like I need to override more Path
methods for NonePath
.
Well, I clearly seem to be out of my depths with python internals here. For the moment, I'll have to use what works and live with the inelegant None
/ Path
approach.
MY QUESTION: Is it possible to implement a NonePath
as explained ? How would I go about it ? Am I at least somewhat on the right track ?
What you need is to specify my_path
as optional:
from pathlib import Path
from typing import Optional
def myfunction(my_path: Optional[Path] = None):
if my_path is None:
print(f'no my_path argument; falling back to some default (or so)')
my_path = Path().resolve()
print(f"{my_path=}")
myfunction()
print("---")
myfunction(Path("/bin"))
When I ran this script from the /tmp dir, I got the following output:
no my_path argument; falling back to some default (or so)
my_path=PosixPath('/private/tmp')
---
my_path=PosixPath('/bin')
The above does not cause any pyright issue