Skip to main content

tmpkit

Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes, async support, and zero dependencies.

CI PyPI Python License Coverage


Table of Contents


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=1 env var — keep all temps for debugging, no code changes.
  • keep=True per-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).
  • .path attributePath object, no more Path(f.name) boilerplate.
  • Async supportasync with temp_file() as f: with async I/O methods.
  • Windows-safe by default — no O_TEMPORARY lock, 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 dest via os.replace() (same filesystem) or shutil.move() (cross-filesystem).
  • On error: temp is deleted, dest is untouched.
  • If dest already 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:

  1. .keep() method — highest priority, always keeps.
  2. keep=True parameter — always keeps.
  3. DEBUG=1 / TMPKIT_DEBUG=1 env var — keeps all temps globally.
  4. keep_on_error=True + exception — keeps only on error.
  5. dest= move — if none of the above trigger, temp is moved to dest on success.
  6. 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 tempfile module — the foundation tmpkit builds upon.
  • contextlib — inspiration for the context manager patterns.
  • Every developer who has written try/finally/shutil.rmtree boilerplate — 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

tmpkit-1.0.0.tar.gz (44.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tmpkit-1.0.0-py3-none-any.whl (20.3 kB view details)

Uploaded Python 3

File details

Details for the file tmpkit-1.0.0.tar.gz.

File metadata

  • Download URL: tmpkit-1.0.0.tar.gz
  • Upload date:
  • Size: 44.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tmpkit-1.0.0.tar.gz
Algorithm Hash digest
SHA256 5ef4ef58f1f45055b17653d33ce7d969e3d60b5ffcc58f5930588b1af350a3e0
MD5 77225aade141396d9370d111a4273a23
BLAKE2b-256 129fdf54dcbb9e228de463e996676992c76b2486e8d0d2672d5609bcb678f0d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for tmpkit-1.0.0.tar.gz:

Publisher: release.yml on MathiasPaulenko/tmpkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tmpkit-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: tmpkit-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 20.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tmpkit-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 95ba563a6bf21d22d828edab04c955d462107547cb2618eda911dd5e7f771778
MD5 a56d083846fedd2a11ce3fae7a23cab4
BLAKE2b-256 6650dd651859ff8c1411f50dddcc3a216947586ba99d35cad449ec245d16828c

See more details on using hashes here.

Provenance

The following attestation bundles were made for tmpkit-1.0.0-py3-none-any.whl:

Publisher: release.yml on MathiasPaulenko/tmpkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.1

2 files

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page