os.fspath

os.fspath is the official way to say "give me this path as a string": str and bytes pass through, Path objects (and anything with __fspath__) are converted, everything else is a TypeError. fsencode and fsdecode translate between str and bytes names using the encoding the OS calls expect.

os functionPython 3.2+ (fspath 3.6+)Live demo
Common call
os.fspath(path_like)
Returns
'data/report.csv'
Replaces
str(path) — which accepts anything, even None
Watch out
Undecodable bytes behave differently: surrogateescape on Linux/macOS, an error on Windows
os.fspath(path) / os.fsencode(filename) / os.fsdecode(filename)
→ str | bytes

Demo

Live evaluation
A file name as the bytes the OS stores, and back again.
Try:
Inputs
namestra file name
Code
import os
raw = os.fsencode('report.txt')
(raw, os.fsdecode(raw))
Result
(b'report.txt', 'report.txt')

fsencode uses UTF-8 here (the file system encoding on Windows and on any modern Linux or macOS), so é becomes b'\xc3\xa9' and the CJK characters three bytes each; fsdecode reverses it exactly. In the fspath tab the str is returned as typed, while the Path object has already dropped the doubled slash, the '.' and the trailing slash — and an empty path becomes '.'.

Parameters

NameTypeRequiredDescription
path / filenamestr | bytes | os.PathLikeyesA path in any accepted form. fsencode returns bytes unchanged; fsdecode returns str unchanged.

Return value

str | bytes — fspath: str or bytes unchanged, otherwise the result of __fspath__(). fsencode: bytes. fsdecode: str.

Common patterns

Accept str or Path in your own API
Normalize once at the boundary.
import os
def load(path):
    path = os.fspath(path)   # str, bytes, Path, DirEntry … all fine
    with open(path, 'rb') as f:
        return f.read()
Make your class path-like
Implement __fspath__ and every os, open() and shutil call accepts it.
import os
class Asset:
    def __init__(self, root, name):
        self.root, self.name = root, name
    def __fspath__(self):
        return os.path.join(self.root, self.name)
Pass a name to a bytes-only API
fsencode produces exactly what the OS call needs.
import os
raw_name = os.fsencode(filename)

Examples

1. A Path becomes its string
import os from pathlib import PurePosixPath os.fspath(PurePosixPath('a//b/./c/'))
Returns
'a/b/c'
2. Your own path-like class
import os class Upload: def __init__(self, name): self.name = name def __fspath__(self): return '/srv/uploads/' + self.name os.fspath(Upload('a.png'))
Returns
'/srv/uploads/a.png'
3. Not a path
import os os.fspath(123)
Returns
TypeError: expected str, bytes or os.PathLike object, not int
4. str → bytes (UTF-8)
import os os.fsencode('café.txt')
Returns
b'caf\xc3\xa9.txt'
5. bytes → str
import os os.fsdecode(b'caf\xc3\xa9.txt')
Returns
'café.txt'
6. Already the right type: unchanged
import os (os.fsencode(b'raw'), os.fsdecode('already str'))
Returns
(b'raw', 'already str')
7. Path is an os.PathLike
import os from pathlib import Path isinstance(Path('x'), os.PathLike)
Returns
True

Pitfalls

1. str() hides mistakes
str(None) is 'None' — a perfectly valid file name. fspath refuses anything that is not a path.
str(path)
path = None
str(path)
'None'
os.fspath(path)
import os
path = None
os.fspath(path)
TypeError: expected str, bytes or os.PathLike object, not NoneType
2. __fspath__ must return str or bytes
fspath checks the result of __fspath__ and raises if it is anything else.
returns an int
import os
class Bad:
    def __fspath__(self):
        return 42
os.fspath(Bad())
TypeError: expected Bad.__fspath__() to return str or bytes, not int
returns a str
import os
class Good:
    def __fspath__(self):
        return 'file42.txt'
os.fspath(Good())
'file42.txt'

When to use

Use it
  • Functions that accept "any path" and need a str or bytes
  • Talking to APIs that want bytes file names (fsencode) or produced them (fsdecode)
Reach for something else
  • Displaying a path to a user → str(path) is fine there
  • Decoding file contents → bytes.decode with the content encoding, not fsdecode

Notes

CPython impl
fspath is implemented in C (Modules/posixmodule.c); fsencode and fsdecode are in Lib/os.py and use sys.getfilesystemencoding() with sys.getfilesystemencodeerrors()
Error handler
Linux and macOS: 'surrogateescape' — undecodable bytes survive a round trip as lone surrogates (os.fsdecode(b'\xff') is '\udcff' there). Windows: 'surrogatepass', and os.fsdecode(b'\xff') raises UnicodeDecodeError
os.PathLike
An abstract base class (3.6+); any class with __fspath__ counts as one. os.PathLike is not in os.__all__ but is public

FAQ

os.fspath(p) or str(p) — both give the same string for a Path. Prefer os.fspath in code that should reject non-paths: str() converts anything, including None.