tmpkit
Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes, async support, and zero dependencies.
Table of Contents
- Why tmpkit?
- Installation
- Quick Start
- Features
- API Reference
- Keep Control: Precedence
- Cleanup Hooks
- Comparison
- Contributing
- Changelog
- Acknowledgements
- License
Why tmpkit?
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)
Installation
pip install tmpkit
Requirements: Python >= 3.11. Zero runtime dependencies.
For development:
pip install -e ".[dev]"
This installs pytest, pytest-asyncio, pytest-cov, ruff, mypy, and build.
Quick Start
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 str
Features
keep_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.
API Reference
temp_file()
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 keep
Returns: 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.
temp_dir()
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.
atomic_write()
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 deleted
Writes 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.
@temp_dir() Decorator
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.
@temp_file() Decorator
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.
temp_registry
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.
Async API
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().
Environment Variables
| 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.
Keep Control: Precedence
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 kept
Cleanup Hooks
The 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
Comparison
| 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 |
Contributing
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.
Changelog
See CHANGELOG.md for a full list of changes.
Acknowledgements
- 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.
License
MIT — Copyright (c) 2025-2026 Mathias Paulenko
Metadata
Release files for tmpkit 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tmpkit-1.1.0.tar.gz | 48.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tmpkit-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 71.3 kB
Release files / tmpkit-1.1.0.tar.gz
| Download URL | tmpkit-1.1.0.tar.gz |
|---|---|
| Size | 48.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
943768927b02bbca8235a3539e8a35c109047167c2b0c0ecfd8c2153839cdb15
|
|
BLAKE2b-256 checksum How to use checksums |
054b84124aef8ac78fc827e0e6ba96019fa2f0a0ebe36b313912f4bf48aa9dc3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / tmpkit-1.1.0-py3-none-any.whl
| Download URL | tmpkit-1.1.0-py3-none-any.whl |
|---|---|
| Size | 22.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8051bd40c9e69e9dfa6ba0df8baecc9e37393960b470ee4c87c35e86e89dbb79
|
|
BLAKE2b-256 checksum How to use checksums |
ebb117764409365589eb7a143b0c79e7ab9e993bc1f1a373bb6e269fc9c2374a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log