Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes, async support, and zero dependencies.
- Why tmpkit?
- Installation
- Quick Start
- Features
- API Reference
- Keep Control: Precedence
- Cleanup Hooks
- Comparison
- Contributing
- Changelog
- Acknowledgements
- License
Python's tempfile gives you the pieces but forces you to write cleanup boilerplate every time. tmpkit wraps it in ergonomic context managers that always attempt cleanup on exit — with features nobody else offers.
# stdlib — verbose, easy to forget cleanup
import os, tempfile, shutil
tmpdir = tempfile.mkdtemp()
try:
with open(os.path.join(tmpdir, "data.csv"), "w") as f:
f.write(data)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
# tmpkit — one line, cleans up on exit
from tmpkit import temp_file
with temp_file(suffix=".csv") as f:
f.write(data)pip install tmpkitRequirements: Python >= 3.11. Zero runtime dependencies.
For development:
pip install -e ".[dev]"This installs pytest, pytest-asyncio, pytest-cov, ruff, mypy, and build.
from tmpkit import temp_file, temp_dir, atomic_write, async_temp_file
# Temp file (default mode is "w+b" — binary; use mode="w+" for str)
with temp_file(suffix=".csv", prefix="myapp_") as f:
f.write(data)
# deleted on exit
# Temp dir with auto-chdir
with temp_dir(cwd=True) as d:
(d / "output.txt").write_text("hello")
# cwd restored, dir removed on exit
# Keep on error — the killer debugging feature
with temp_file(keep_on_error=True) as f:
f.write(data)
risky_operation(f) # if this raises, file stays
# if no exception, file is deleted
# Atomic write
with atomic_write("config.json") as f:
f.write(data)
# on success: atomically renamed to config.json
# on error: config.json untouched, temp cleaned up
# Promote temp to permanent location
with temp_file(dest="output.csv") as f:
f.write(data)
# on success: moved to output.csv
# Async
async def main() -> None:
async with async_temp_file(suffix=".json") as f:
await f.write(data) # bytes by default; mode="w+" for strkeep_on_error=True— keep temp only on exception, delete on success. The #1 most requested tempfile feature. Nobody else has it.atomic_write()— temp file + atomic rename. The most reimplemented pattern, now built-in.dest=parameter — promote temp to a permanent location on success.DEBUG=1env var — keep all temps for debugging, no code changes.keep=Trueper-call — keep a specific temp without global DEBUG..keep()method — decide at runtime whether to keep.cwd=True— auto-chdir into temp dir, restore on exit.content=— pre-populate file with content.cleanup_hook=— custom hook called before standard cleanup.temp_registry— track all temps globally, cleanup on demand.@temp_dir()/@temp_file()decorators — inject temps into functions and test classes.- Close without delete — file survives
close(), deleted on context exit (Windows subprocess friendly). .pathattribute —Pathobject, no morePath(f.name)boilerplate.- Async support —
async with async_temp_file() as f:with async I/O methods. - Windows-safe by default — no
O_TEMPORARYlock,ignore_cleanup_errors=True. - Zero dependencies — stdlib only.
from tmpkit import temp_file
with temp_file(
suffix: str | None = None, # e.g. ".csv"
prefix: str | None = None, # e.g. "myapp_"
dir: str | Path | None = None, # parent directory
mode: str = "w+b", # open mode
content: str | bytes | None = None, # pre-populate
dest: str | Path | None = None, # move here on success (parent must exist at exit)
keep: bool = False, # always keep
keep_on_error: bool = False, # keep only on exception
ignore_cleanup_errors: bool = True,
cleanup_hook: Callable[[Path], None] | None = None,
) as f:
f.write(data) # file-like I/O
f.read()
f.seek(0)
f.path # Path object
f.keep() # runtime decision to keepReturns: A file-like object with .path (Path), .keep(), and all standard file methods (read, write, seek, tell, flush, close).
dest= behavior:
- On success: temp is moved to
destviaos.replace()(same filesystem) orshutil.move()(cross-filesystem). - On error: temp is deleted,
destis untouched. - If
destalready exists, it is overwritten.
from tmpkit import temp_dir
with temp_dir(
suffix: str | None = None,
prefix: str | None = None,
dir: str | Path | None = None,
cwd: bool = False, # auto-chdir into temp dir
keep: bool = False,
keep_on_error: bool = False,
ignore_cleanup_errors: bool = True,
cleanup_hook: Callable[[Path], None] | None = None,
) as d:
(d / "file.txt").write_text("hello")
d # Path object
# To call .keep(), use the context manager object directly:
td = temp_dir()
with td as d:
(d / "file.txt").write_text("hello")
td.keep() # runtime decision to keep (must be inside the block)Returns: A Path object (the temp directory path) with / operator support. To call .keep(), use the context manager object directly (see example above) — it must be called inside the with block; calling it after exit raises RuntimeError.
cwd=True: Changes the working directory to the temp dir on __enter__, restores the original on __exit__. Warning: os.chdir is process-global — do not use cwd=True from multiple threads or concurrent async tasks.
from tmpkit import atomic_write
with atomic_write(
dest: str | Path, # final destination
mode: str = "w", # "w" (text) or "wb" (binary)
encoding: str | None = None,
newline: str | None = None,
prefix: str | None = None,
suffix: str = ".tmp",
fsync: bool = True, # fsync before rename
keep: bool = False, # keep temp, dest untouched
keep_on_error: bool = False,
ignore_cleanup_errors: bool = True,
cleanup_hook: Callable[[Path], None] | None = None,
) as f:
f.write(data)
# on success: atomically renamed to dest
# on error: dest untouched, temp deletedWrites to a temp file in dest's parent directory, then atomically replaces dest via os.replace() on success. On error, the temp is cleaned up and dest is left untouched.
Note: with keep=True, .keep(), keep_on_error=True + exception, or DEBUG=1, the temp file is kept and the replace is skipped — dest is left untouched even on success.
Validation on enter: raises IsADirectoryError if dest is an existing directory, FileNotFoundError if dest's parent doesn't exist, and NotADirectoryError if the parent exists but isn't a directory — before writing anything.
from tmpkit import temp_dir_decorator
# On a function — temp dir injected as first arg
@temp_dir_decorator()
def process(tmp: Path, data: str) -> None:
(tmp / "output.txt").write_text(data)
# cwd=True by default for the decorator
@temp_dir_decorator(prefix="test_")
def my_func(tmp: Path) -> str:
return str(tmp)
# On a test class — each test_ method gets a fresh temp dir
@temp_dir_decorator()
class TestMyApp:
def test_writes_file(self) -> None:
assert self.tmpdir.exists()
(self.tmpdir / "data.txt").write_text("test")
async def test_async(self) -> None:
assert self.tmpdir.exists()Decorator defaults: cwd=True (unlike the context manager where cwd=False by default). Accepts the same parameters as temp_dir(), including cleanup_hook. Only plain functions and classes — decorating a single method injects the temp path in place of self.
Class decoration: Each method starting with test_ is wrapped (staticmethod/classmethod members are skipped). The temp dir is available as self.tmpdir. Works with both sync and async test methods.
from tmpkit import temp_file_decorator
@temp_file_decorator(mode="w+")
def process(f, data: str) -> str:
f.write(data)
f.seek(0)
return f.read()
result = process("hello world")
# Async functions supported
@temp_file_decorator(suffix=".json")
async def process_async(f, data: str) -> None:
await f.write(data)The temp file object is injected as the first positional argument. Accepts the same parameters as temp_file(), including cleanup_hook. Plain functions only — decorating a class raises TypeError.
from tmpkit import temp_registry
# Enable tracking
temp_registry.enable()
# Or via env var: TMPKIT_REGISTRY=1
with temp_file() as f:
assert len(temp_registry.active) == 1
assert temp_registry.active[0].path == f.path
# After exit:
assert len(temp_registry.active) == 0
assert len(temp_registry.cleaned) == 1
# Inspect all records
for record in temp_registry.all:
print(f"{record.kind} at {record.path} (cleaned={record.cleaned}, kept={record.kept})")
# Emergency cleanup
count = temp_registry.cleanup_all() # deletes all active temps
# Mark all as kept
temp_registry.keep_all()
# Clear cleaned records from history
temp_registry.clear_history()
# Disable
temp_registry.disable()TempRecord is importable for type annotations: from tmpkit import TempRecord. Fields:
| Field | Type | Description |
|---|---|---|
path |
Path |
Filesystem path |
kind |
"file" or "dir" |
Resource type |
created_at |
float |
Epoch timestamp |
cleaned |
bool |
Whether it was cleaned up |
kept |
bool |
Whether it was kept |
Thread-safe: All operations are protected by threading.Lock.
Note: records accumulate for the lifetime of the process. In long-running applications, call temp_registry.clear_history() periodically (or reset()) to bound memory usage.
All sync APIs have async counterparts with identical parameters:
from tmpkit import async_temp_file, async_temp_dir, async_atomic_write
# Async temp file
async with async_temp_file(suffix=".csv") as f:
await f.write(data)
await f.seek(0)
content = await f.read()
# Async temp dir
async with async_temp_dir(cwd=True) as d:
...
# Async atomic write
async with async_atomic_write("config.json") as f:
await f.write(data)Async file objects support await f.read(), await f.write(), await f.seek(), await f.tell(), await f.flush(), await f.close().
| Variable | Value | Effect |
|---|---|---|
TMPKIT_DEBUG |
1 |
Keep all temps (overrides keep=False) |
DEBUG |
1 |
Same as TMPKIT_DEBUG=1 (fallback) |
TMPKIT_REGISTRY |
1 |
Enable temp_registry at import time |
TMPKIT_DEBUG takes precedence over DEBUG. Since DEBUG is a common variable name in other tools, you can set TMPKIT_DEBUG=0 to explicitly disable keep-all even when DEBUG=1 is present.
When multiple keep signals are present, precedence is:
.keep()method — highest priority, always keeps.keep=Trueparameter — always keeps.DEBUG=1/TMPKIT_DEBUG=1env var — keeps all temps globally.keep_on_error=True+ exception — keeps only on error.dest=move — if none of the above trigger, temp is moved to dest on success.- Standard cleanup — temp is deleted.
# .keep() wins over everything
with temp_file(keep=False) as f:
f.write(data)
f.keep() # file is kept despite keep=False
# DEBUG=1 overrides keep=False
# $ TMPKIT_DEBUG=1 python my_script.py
with temp_file() as f: # file is kept
f.write(data)
# keep_on_error keeps only on exception
with temp_file(keep_on_error=True) as f:
f.write(data)
raise RuntimeError("oops") # file is keptThe cleanup_hook parameter lets you run custom logic before standard cleanup:
def my_hook(path: Path) -> None:
print(f"Cleaning up {path}")
# e.g. log, collect metrics, copy to backup, etc.
with temp_file(cleanup_hook=my_hook) as f:
f.write(data)
# hook is called, then standard cleanup runs
# Hook also runs when the temp is moved via dest=
with temp_file(dest="output.csv", cleanup_hook=my_hook) as f:
f.write(data)
# hook is called, then temp is moved to output.csv
# Hook is called even on exceptions
with temp_file(cleanup_hook=my_hook) as f:
raise RuntimeError("oops")
# hook is still called, then temp is deleted
# Hook errors are swallowed if ignore_cleanup_errors=True (default)
def bad_hook(path: Path) -> None:
raise OSError("hook failed")
with temp_file(cleanup_hook=bad_hook) as f: # no error raised
f.write(data)
# Hook is NOT called when temp is kept
with temp_file(keep=True, cleanup_hook=my_hook) as f:
f.write(data)
# hook is NOT called| Feature | tmpkit | stdlib tempfile |
temporary |
tdir |
temppathlib |
ephemdir |
|---|---|---|---|---|---|---|
keep_on_error |
Yes | No | No | No | No | No |
atomic_write() |
Yes | No | No | No | No | No |
dest= promote |
Yes | No | No | No | No | No |
cleanup_hook |
Yes | No | No | No | No | No |
temp_registry |
Yes | No | No | No | No | No |
| Decorators | Yes | No | No | No | No | No |
DEBUG=1 env var |
Yes | No | No | No | No | No |
keep=True per-call |
Yes | No | No | No | No | No |
.keep() method |
Yes | No | No | No | No | No |
| Close without delete | Yes | No | No | No | No | No |
.path attribute |
Yes | No | No | No | Yes | No |
cwd=True |
Yes | No | Yes | Yes | No | No |
content= |
Yes | No | Yes | No | No | No |
| Async support | Yes | No | No | No | No | No |
| Windows-safe by default | Yes | No | No | No | No | No |
ignore_cleanup_errors |
Yes (default) | Yes (opt-in) | No | No | No | No |
Returns Path |
Yes | No | Yes | No | Yes | No |
| Zero deps | Yes | Yes | No | Yes | Yes | Yes |
| Python >=3.11 | Yes | Yes | No | Yes | No | Yes |
See CONTRIBUTING.md for development setup, code style, testing, and pull request guidelines.
Please read our Code of Conduct before contributing.
To report a security vulnerability, see SECURITY.md.
See CHANGELOG.md for a full list of changes.
- Python's
tempfilemodule — the foundation tmpkit builds upon. contextlib— inspiration for the context manager patterns.- Every developer who has written
try/finally/shutil.rmtreeboilerplate — you deserved better.
MIT — Copyright (c) 2025-2026 Mathias Paulenko