os.path.expanduser

The shell expands ~ and $HOME for you; Python's open() does not. These two functions do the expansion yourself. Both read the environment of the running process, so their result is different on every machine: os.path is posixpath on Linux and macOS (HOME, $name) and ntpath on Windows (USERPROFILE, %name% as well).

os.path functionPython 3 (path-like arguments 3.6+, HOME ignored on Windows 3.8+)
Common call
os.path.expanduser('~/.config/app.toml')
Returns
'/home/ada/.config/app.toml' on Linux (your own home)
Replaces
os.environ['HOME'] + path[1:] and hand-written $VAR substitution
Watch out
Only a LEADING ~ is expanded, and unknown $VARS stay in the string silently
os.path.expanduser(path) / os.path.expandvars(path)
→ str | bytes

Parameters

NameTypeRequiredDescription
pathstr | bytes | os.PathLikeyesThe path to expand. A path-like object (3.6+) is accepted; the result is always str or bytes, never a Path.

Return value

str | bytes — The path with ~ (expanduser) or $variables (expandvars) replaced; unchanged when nothing could be expanded.

Common patterns

Config file in the home directory
The usual first line of a CLI tool that keeps settings under ~.
import os
config = os.path.expanduser('~/.config/myapp/config.toml')
Expand both ~ and $VARS from user input
Run expandvars first, then expanduser, so a value like ~/$PROJECT becomes a full path.
import os

def expand(path):
    return os.path.expanduser(os.path.expandvars(path))
The pathlib way
Path.expanduser() and Path.home() do the same lookup; there is no pathlib expandvars.
from pathlib import Path
config = Path('~/.config/myapp/config.toml').expanduser()
home = Path.home()
Detect that a variable was not set
expandvars leaves unknown names in place, so check the result (or read os.environ directly).
import os
log_dir = os.path.expandvars('$APP_LOG_DIR')
if log_dir.startswith('$'):
    raise SystemExit('APP_LOG_DIR is not set')

Examples

1. A leading ~ becomes HOME (posixpath)
import os, posixpath from unittest import mock with mock.patch.dict(os.environ, {'HOME': '/home/ada'}): result = posixpath.expanduser('~/notes.txt') result
Returns
'/home/ada/notes.txt'
2. Only a leading tilde counts
import os, posixpath from unittest import mock with mock.patch.dict(os.environ, {'HOME': '/home/ada'}): result = [posixpath.expanduser(p) for p in ['~', 'a/~/b', '/tmp/~x', 'notes/~']] result
Returns
['/home/ada', 'a/~/b', '/tmp/~x', 'notes/~']
3. Windows rules: USERPROFILE (ntpath)
import os, ntpath from unittest import mock with mock.patch.dict(os.environ, {'USERPROFILE': r'C:\Users\ada'}): result = [ntpath.expanduser(p) for p in ['~', r'~\notes.txt']] result
Returns
['C:\\Users\\ada', 'C:\\Users\\ada\\notes.txt']
4. Windows ~user is a guess next to your own home
import os, ntpath from unittest import mock with mock.patch.dict(os.environ, {'USERPROFILE': r'C:\Users\ada', 'USERNAME': 'ada'}): result = ntpath.expanduser('~bob') result
Returns
'C:\\Users\\bob'
5. $name and ${name} (posixpath)
import os, posixpath from unittest import mock with mock.patch.dict(os.environ, {'APP_DIR': '/srv/app'}): result = [posixpath.expandvars(p) for p in ['$APP_DIR/logs', '${APP_DIR}/logs', '%APP_DIR%/logs']] result
Returns
['/srv/app/logs', '/srv/app/logs', '%APP_DIR%/logs']
6. ntpath also understands %name%
import os, ntpath from unittest import mock with mock.patch.dict(os.environ, {'APP_DIR': r'D:\app'}): result = [ntpath.expandvars(p) for p in [r'%APP_DIR%\logs', r'$APP_DIR\logs', r'${APP_DIR}\logs']] result
Returns
['D:\\app\\logs', 'D:\\app\\logs', 'D:\\app\\logs']
7. Unknown variables are left alone
import posixpath, ntpath (posixpath.expandvars('$NO_SUCH_VAR_Q7/x'), ntpath.expandvars('%NO_SUCH_VAR_Q7%'))
Returns
('$NO_SUCH_VAR_Q7/x', '%NO_SUCH_VAR_Q7%')
8. Path-like in, str out
import os, posixpath from pathlib import PurePosixPath from unittest import mock with mock.patch.dict(os.environ, {'HOME': '/home/ada'}): result = posixpath.expanduser(PurePosixPath('~/x')) result
Returns
'/home/ada/x'

Pitfalls

1. Expecting open() to understand ~
~ is a shell feature. open('~/notes.txt') looks for a folder literally named ~ in the current directory. Expand it first.
open('~/...')
try:
    open('~/notes.txt')
except OSError as e:
    result = type(e).__name__
result
'FileNotFoundError'
expanduser first
import os, posixpath
from unittest import mock
with mock.patch.dict(os.environ, {'HOME': '/home/ada'}):
    result = posixpath.expanduser('~/notes.txt')
result
'/home/ada/notes.txt'
2. A variable name runs into the following text
$name takes every letter, digit and underscore that follows, so $NAME_backup looks up NAME_backup, which is not set, and stays as it is. Use braces.
$NAME_backup
import os, posixpath
from unittest import mock
with mock.patch.dict(os.environ, {'NAME': 'ada'}):
    result = posixpath.expandvars('$NAME_backup')
result
'$NAME_backup'
${NAME}_backup
import os, posixpath
from unittest import mock
with mock.patch.dict(os.environ, {'NAME': 'ada'}):
    result = posixpath.expandvars('${NAME}_backup')
result
'ada_backup'
3. Each function does only its own half
expanduser ignores $HOME and expandvars ignores ~. Chain them when input can contain both.
one call each
import os, posixpath
from unittest import mock
with mock.patch.dict(os.environ, {'HOME': '/home/ada'}):
    result = posixpath.expanduser('$HOME/x'), posixpath.expandvars('~/x')
result
('$HOME/x', '~/x')
chain them
import os, posixpath
from unittest import mock
with mock.patch.dict(os.environ, {'HOME': '/home/ada'}):
    result = [posixpath.expanduser(posixpath.expandvars(p)) for p in ['$HOME/x', '~/x']]
result
['/home/ada/x', '/home/ada/x']

When to use

Use it
  • Turning user-typed or config-file paths like ~/data or $XDG_CONFIG_HOME/app into real paths
  • Building per-user default locations for config, cache and history files
Reach for something else
  • Reading one variable → os.environ.get(name) tells you when it is missing
  • Untrusted input → expandvars can pull any environment value into the path
  • Object-oriented code → Path.expanduser() / Path.home() (no expandvars equivalent)

Notes

CPython impl
Pure Python in Lib/posixpath.py and Lib/ntpath.py; os.path is one of these two modules, chosen when os is imported
Linux and macOS
posixpath: ~ uses HOME if set, otherwise the password database (pwd module); ~user is looked up in the password database. If the lookup fails the path comes back unchanged
Windows
ntpath: ~ uses USERPROFILE, otherwise HOMEDRIVE + HOMEPATH. HOME is not used since 3.8. ~user only swaps the last folder of your own home for user (if your home ends in USERNAME), so it is a guess, not a lookup
expandvars
Unknown or malformed names are left unchanged, never an error. ntpath also expands %name% and leaves text inside single quotes alone

FAQ

Because it is different on every machine. The examples set HOME or USERPROFILE inside unittest.mock.patch.dict(os.environ, ...) and call posixpath or ntpath directly, so the output is the same everywhere. In your own code just call os.path.expanduser, which is posixpath.expanduser on Linux and macOS and ntpath.expanduser on Windows.