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 guarantee cleanup — 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, always cleans up
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
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)
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 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
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
Returns: A Path object (the temp directory path) with / operator support. To call .keep(), use the context manager object directly (see example above).
cwd=True: Changes the working directory to the temp dir on __enter__, restores the original on __exit__.
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_on_error: bool = False,
ignore_cleanup_errors: bool = True,
) 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.
@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).
Class decoration: Each method starting with test_ is wrapped. 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.
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 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.
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.
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 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 Mathias Paulenko
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tmpkit-1.0.1.tar.gz.
File metadata
- Download URL: tmpkit-1.0.1.tar.gz
- Upload date:
- Size: 44.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16ef2c67cc31ffec767edc6083674756f9b603079b1c5f526c179de6d21732bd
|
|
| MD5 |
51e692dab276bb5e0e39ac84b30cfd21
|
|
| BLAKE2b-256 |
4754f4735de9a6d301dc1bbf3b42aa666e7b30e21c5fe746230330c84fe55aa9
|
Provenance
The following attestation bundles were made for tmpkit-1.0.1.tar.gz:
Publisher:
release.yml on MathiasPaulenko/tmpkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tmpkit-1.0.1.tar.gz -
Subject digest:
16ef2c67cc31ffec767edc6083674756f9b603079b1c5f526c179de6d21732bd - Sigstore transparency entry: 2582543675
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/tmpkit@285ccb91013034eba7448a032e931f0fdaaaa411 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@285ccb91013034eba7448a032e931f0fdaaaa411 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tmpkit-1.0.1-py3-none-any.whl.
File metadata
- Download URL: tmpkit-1.0.1-py3-none-any.whl
- Upload date:
- Size: 20.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95b206e64168b074cdbfa4e0d874dd05c141bf30e065aaf74339862c2a02c162
|
|
| MD5 |
9f739cd52b81630cca67f33bded199cc
|
|
| BLAKE2b-256 |
f0a8570c915ada5012c7f7b875955d5a22ce90be40735baa69b5c407c33d388c
|
Provenance
The following attestation bundles were made for tmpkit-1.0.1-py3-none-any.whl:
Publisher:
release.yml on MathiasPaulenko/tmpkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tmpkit-1.0.1-py3-none-any.whl -
Subject digest:
95b206e64168b074cdbfa4e0d874dd05c141bf30e065aaf74339862c2a02c162 - Sigstore transparency entry: 2582543683
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/tmpkit@285ccb91013034eba7448a032e931f0fdaaaa411 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@285ccb91013034eba7448a032e931f0fdaaaa411 -
Trigger Event:
push
-
Statement type: