Accepting paths in your own APIs

pathlib · Expert · 9 min

Library code that only works with one kind of path frustrates users and breaks in surprising ways.

Callers will pass strings, Path objects, and occasionally other os.PathLike objects (from libraries like py.path or custom classes). A robust function accepts all of them:

def load(path: str | os.PathLike[str]) -> dict:
    path = Path(path)

Rules of thumb:

  • Normalise at the boundary: convert once with Path(path) at the top, then work with Path internally.
  • If you need a string (for a C library or a subprocess argument), use os.fspath(path) rather than str(path). fspath accepts only str, bytes and PathLike and raises TypeError for other objects, whereas str() happily turns None or an int into a bogus path.
  • Type hints: str | os.PathLike[str] is the common annotation; typeshed's StrPath alias means the same.
  • Return Path objects from functions that produce paths; they convert to strings trivially.
  • Don't accept bytes paths unless you really need to handle undecodable names; mixing str and bytes in one join raises TypeError.

Avoid isinstance(x, str) checks to decide whether something is a path: a str might be file contents. Make two clearly named parameters or functions instead (load_file(path) vs loads(text)), the way json does.

Example

import os
from pathlib import Path

def word_count(path: str | os.PathLike[str]) -> int:
    path = Path(path)                       # normalise once
    return len(path.read_text(encoding="utf-8").split())

class Asset:
    def __init__(self, name): self.name = name
    def __fspath__(self): return f"assets/{self.name}"

Path("assets").mkdir()
Path("assets/a.txt").write_text("one two three", encoding="utf-8")
print(word_count("assets/a.txt"))           # 3
print(word_count(Path("assets", "a.txt")))  # 3
print(word_count(Asset("a.txt")))           # 3

print(str(None))                            # None  -- a bogus "path"
try:
    os.fspath(None)
except TypeError as e:
    print(type(e).__name__)                 # TypeError

Remember

  • Accept str | os.PathLike[str] and convert with Path(...) at the boundary
  • Use os.fspath instead of str() when you need a string path
  • Return Path objects; don't overload one parameter for both paths and contents

Watch out for

  • Using str(path) for conversion, which turns a stray None into a file literally named 'None'.

Check yourself

  1. Why prefer os.fspath(p) over str(p) inside a function that takes a path?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/os.html#os.fspath

Builds on

Unlocks

Nothing builds on this yet.