How-to guides

Recipes for common tasks. For background on why directories differ across platforms, see Understanding platformdirs.

Common patterns

Creating directories safely

Always create parent directories before writing files:

from pathlib import Path
from platformdirs import user_data_path

data_dir = user_data_path("MyApp")
db_file = data_dir / "data.db"

# Create directory if it doesn't exist
db_file.parent.mkdir(parents=True, exist_ok=True)

# Now safe to write
db_file.write_bytes(b"...")

Or use ensure_exists=True to create directories automatically:

from platformdirs import PlatformDirs

dirs = PlatformDirs("MyApp", ensure_exists=True)
db_file = dirs.user_data_path / "data.db"
db_file.write_bytes(b"...")  # Directory already exists

Placing a file in a user directory

place_config_file() returns the path of a file under user_config_dir and creates the missing directories on the way with mode 0o700, as the XDG base directory specification asks. The process umask still applies to that mode. Directories that already exist keep their mode, and you write the file yourself:

from platformdirs import PlatformDirs

dirs = PlatformDirs("MyApp")
path = dirs.place_config_file("profiles/default.toml")
path.write_text('theme = "dark"\n', encoding="utf-8")

place_data_file, place_cache_file, place_state_file, place_log_file and place_runtime_file do the same for the matching user_*_dir. A name that is empty, absolute, has a drive or climbs out with .. raises ValueError.

Handling write errors

Directory paths may not be writable due to permissions or disk space:

from platformdirs import user_data_path
from pathlib import Path
import tempfile

data_dir = user_data_path("MyApp")
data_file = data_dir / "data.json"

try:
    data_dir.mkdir(parents=True, exist_ok=True)
    data_file.write_text('{"key": "value"}')
except (OSError, PermissionError) as e:
    # Fallback to temp directory
    temp_dir = Path(tempfile.gettempdir()) / "MyApp"
    temp_dir.mkdir(parents=True, exist_ok=True)
    data_file = temp_dir / "data.json"
    data_file.write_text('{"key": "value"}')

Checking directory writability

Test if a directory is writable before using it:

from platformdirs import user_cache_path
import os

cache_dir = user_cache_path("MyApp")
cache_dir.mkdir(parents=True, exist_ok=True)

if os.access(cache_dir, os.W_OK):
    # Directory is writable
    cache_file = cache_dir / "cache.dat"
    cache_file.write_bytes(b"...")
else:
    # Handle read-only directory
    print(f"Warning: {cache_dir} is not writable")

Cleaning up cache

Implement cache cleanup based on age or size:

from platformdirs import user_cache_path
import time

cache_dir = user_cache_path("MyApp")
max_age_days = 30

if cache_dir.exists():
    now = time.time()
    for item in cache_dir.rglob("*"):
        if item.is_file():
            age_days = (now - item.stat().st_mtime) / 86400
            if age_days > max_age_days:
                item.unlink()

Versioned data migration

When using the version parameter, handle migration between versions:

from platformdirs import user_data_path
import shutil

current_version = "2.0"
previous_version = "1.0"

current_dir = user_data_path("MyApp", version=current_version)
previous_dir = user_data_path("MyApp", version=previous_version)

if not current_dir.exists() and previous_dir.exists():
    # Migrate data from previous version
    shutil.copytree(previous_dir, current_dir)

Merging config from multiple sources

Use iter_config_paths() to load site-wide defaults first, then overlay user-specific overrides:

import json
from platformdirs import PlatformDirs

dirs = PlatformDirs("MyApp")
config = {}

# The iterators yield the user directory first, so apply them in reverse to let it win
for config_dir in reversed(list(dirs.iter_config_paths())):
    config_file = config_dir / "config.json"
    if config_file.exists():
        config.update(json.loads(config_file.read_text()))

The same pattern works with iter_data_paths() for data files and iter_config_dirs() for string paths.

Finding a file in user or site directories

Use find_config_file() to load the copy of a file that wins under the XDG Base Directory precedence rule, user directory before site directories, and find_config_files() to get every copy in that order:

from platformdirs import PlatformDirs

dirs = PlatformDirs("MyApp", "Acme")
if (config_file := dirs.find_config_file("config.toml")) is not None:
    print(config_file.read_text())

for plugin_file in dirs.find_config_files("plugins/enabled.toml"):
    print(plugin_file)

Both search the directories of iter_config_paths() and match regular files only, so a directory named config.toml is skipped. They never create a directory, even with ensure_exists=True. The name may contain subdirectories; an absolute path, a drive or a .. component raises ValueError. The same pair exists for data, cache, state, log and runtime files, e.g. find_data_file().

Testing code that uses platformdirs

Wrap the test in platformdirs.testing.isolated_dirs() to resolve every directory under a temporary root, so the test never reads or writes the real home and system folders:

import json

from platformdirs import PlatformDirs


def my_app_init(dirs: PlatformDirs) -> dict:
    config_file = dirs.user_config_path / "config.json"
    if config_file.exists():
        return json.loads(config_file.read_text())
    return {}
# test_my_app.py
from platformdirs import PlatformDirs
from platformdirs.testing import isolated_dirs


def test_loads_config(tmp_path):
    with isolated_dirs(tmp_path):
        config_file = (
            PlatformDirs("MyApp", ensure_exists=True).user_config_path / "config.json"
        )
        config_file.write_text('{"theme": "dark"}')

        assert my_app_init(PlatformDirs("MyApp")) == {"theme": "dark"}

With pytest, request the platformdirs_isolated fixture instead. platformdirs registers it as a pytest plugin; it isolates under tmp_path and yields the root:

def test_loads_config(platformdirs_isolated):
    config_file = (
        PlatformDirs("MyApp", ensure_exists=True).user_config_path / "config.json"
    )
    config_file.write_text('{"theme": "dark"}')

    assert my_app_init(PlatformDirs("MyApp")) == {"theme": "dark"}

Each kind lands in <root>/<kind>, for example <root>/user_config/MyApp, and the module-level functions follow the same layout. The redirection covers PlatformDirs in the current process only: a subprocess, or another platform class such as Unix on macOS, still resolves the real directories.

Overriding directories with environment variables

On Linux/macOS, set XDG environment variables to redirect directories:

$ export XDG_CONFIG_HOME="$HOME/.myconfig"
$ python -c "from platformdirs import user_config_dir; print(user_config_dir('MyApp'))"
/home/user/.myconfig/MyApp

On Windows, use WIN_PD_OVERRIDE_* variables:

> set WIN_PD_OVERRIDE_LOCAL_APPDATA=X:\appdata
> python -c "from platformdirs import user_cache_dir; print(user_cache_dir('MyApp', 'Acme'))"
X:\appdata\Acme\MyApp\Cache

See XDG environment variables for the full list of supported XDG variables, and the Windows section for all WIN_PD_OVERRIDE_* variables.

Platform-specific recipes

Windows: controlling the author directory

On Windows, platformdirs creates a two-level directory structure: AppData\Local\<appauthor>\<appname>. When appauthor is omitted (None), it defaults to appname, producing a doubled path like AppData\Local\MyApp\MyApp. Choose the right value based on your needs:

from platformdirs import user_data_dir

# Explicit author -- AppData\Local\AcmeCompany\MyApp
user_data_dir("MyApp", "AcmeCompany")

# No author -- AppData\Local\MyApp (flat structure)
user_data_dir("MyApp", appauthor=False)

# Default (appauthor=None) -- AppData\Local\MyApp\MyApp
# appauthor falls back to appname
user_data_dir("MyApp")

Use an explicit author string when you publish multiple apps under the same organization. Use appauthor=False when you want a flat structure with just the app name. On non-Windows platforms, appauthor is always ignored.

Windows: roaming vs local profiles

Use roaming=True for settings that should sync across domain-joined machines:

from platformdirs import user_config_path, user_cache_path

# Synced across domain computers
roaming_cfg = user_config_path("MyApp", roaming=True)

# Local to this machine
local_cache = user_cache_path("MyApp")

macOS: separating data and config

On macOS, user_data_dir and user_config_dir both resolve to ~/Library/Application Support/AppName. Use subdirectories to separate concerns:

from platformdirs import user_data_path

app_dir = user_data_path("MyApp")
config_dir = app_dir / "config"
databases_dir = app_dir / "databases"

Linux: falling back from site to user config

Site directories require root privileges. Fall back to user directories for normal users:

from platformdirs import site_config_path, user_config_path
import os

site_cfg = site_config_path("MyApp") / "defaults.conf"

if os.access(site_cfg.parent, os.W_OK):
    site_cfg.write_text("[defaults]\n")
else:
    user_cfg = user_config_path("MyApp") / "config.conf"
    user_cfg.parent.mkdir(parents=True, exist_ok=True)
    user_cfg.write_text("[user_settings]\n")

Linux: redirecting user directories when running as root

Use use_site_for_root=True so system services write to /usr/local/share instead of /root:

from platformdirs import PlatformDirs
import os

if os.geteuid() == 0:
    dirs = PlatformDirs("myservice", use_site_for_root=True)
    # user_data_dir now returns /usr/local/share/myservice
else:
    dirs = PlatformDirs("myservice")
    # user_data_dir returns ~/.local/share/myservice