Path portability
Why string path concatenation breaks across platforms and how pathlib.Path keeps your code correct on Linux, macOS, and Windows.
- Explain why string path concatenation fails on Windows
- Use pathlib.Path and the / operator for all path construction
- Use Path.home() and Path.cwd() for user-relative and working-directory-relative paths
- Use platformdirs to locate the correct config and cache directories on each OS
The most common source of Windows breakage in Python tools written on Linux is
path construction. 'data/' + 'file.csv' produces a forward-slash path that
Windows cannot always parse correctly. String concatenation with os.path.join
works but is verbose. pathlib.Path replaces both approaches with an operator
that is correct on every platform by construction.
The separator problem
Windows uses \ as a path separator; Linux and macOS use /. Python's string
operations know nothing about this distinction:
# Breaks on Windows — produces 'data/file.csv' which Windows tolerates in some
# contexts but fails in others, and '\' is never produced for subdirectories.
path = 'data/' + 'file.csv'
# Also fragile — you must remember every edge case.
path = os.path.join('data', 'subdir', 'file.csv')pathlib.Path uses the correct separator for the current OS automatically:
from pathlib import Path
path = Path('data') / 'subdir' / 'file.csv'
# On Linux/macOS: data/subdir/file.csv
# On Windows: data\subdir\file.csvThe / operator on Path objects is path joining, not division. It reads
naturally and handles every separator edge case correctly.
Absolute paths for the user's home directory
Path.home() returns the user's home directory on every platform without
inspecting environment variables or hard-coding paths:
config_file = Path.home() / '.config' / 'mytool' / 'settings.toml'On Linux this produces /home/alice/.config/mytool/settings.toml; on macOS
/Users/alice/.config/mytool/settings.toml; on Windows
C:\Users\alice\.config\mytool\settings.toml. The code is identical on all three.
Path.cwd() returns the current working directory, equivalent to os.getcwd()
but returning a Path object ready for further composition.
Platform-correct config and cache directories with platformdirs
Placing config files in ~/.config is correct on Linux but not on macOS (which
prefers ~/Library/Application Support) or Windows (%APPDATA%). The
platformdirs library handles this without any conditional logic:
from platformdirs import user_config_dir, user_cache_dir
config_dir = Path(user_config_dir("mytool", "MyOrg"))
cache_dir = Path(user_cache_dir("mytool", "MyOrg"))
config_dir.mkdir(parents=True, exist_ok=True)| Platform | user_config_dir result |
|---|---|
| Linux | ~/.config/mytool |
| macOS | ~/Library/Application Support/mytool |
| Windows | C:\Users\alice\AppData\Roaming\MyOrg\mytool |
Install platformdirs with pip install platformdirs. It is a zero-dependency
package and is already a transitive dependency of many common tools, so it is
rarely an additional install in practice.
Use Path objects throughout your code — open files with open(path), pass
paths to other functions as Path objects, and avoid converting back to strings
unless a third-party library explicitly requires it. If it does, call str(path);
do not rebuild the string by hand.
Practical rule
One simple rule covers most cases: never construct a path with string
concatenation or f-strings. Use Path(...) / part / part for every path, and
Path.home() or platformdirs for any path that depends on the user's
environment. That is the entire discipline of path portability.
Where to go next
Next: platform-specific behaviour — case sensitivity, line endings, signals, and how to detect the current OS when conditional logic is genuinely unavoidable.
Lab: Plugin system
Add a plugin system to a CLI by defining a Processor Protocol, writing two built-in processors, registering them via entry points, and chaining them at runtime.
Platform-specific behaviour
Case sensitivity, line endings, signal handling, and how to detect the current OS when conditional logic is unavoidable.