Search code examples
pythonpython-3.xtypespathlib

Python: How to make a pathlib Path function argument optional?


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 ?


Solution

  • 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