Temporary files appear in more programs than their name suggests. A command-line tool may need scratch space while transforming a large file. A test may need an isolated directory. A program may need to hand a real filesystem path to another process and remove it afterward.

The risky part is not writing the bytes. It is choosing a name, creating the file without a race, deciding who owns cleanup, and handling differences between a file object and a filesystem path.

Python’s tempfile module provides primitives for those jobs. The useful mental model is to choose the API from two questions: does another component need a pathname, and who is responsible for cleanup?

When a task needs several scratch files, TemporaryDirectory is often the simplest boundary:

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as temp_dir:
    workspace = Path(temp_dir)
    input_path = workspace / "input.txt"
    output_path = workspace / "output.txt"

    input_path.write_text("alpha\nbeta\n", encoding="utf-8")
    output_path.write_text(
        input_path.read_text(encoding="utf-8").upper(),
        encoding="utf-8",
    )

    print(output_path.read_text(encoding="utf-8"))

TemporaryDirectory creates a directory and returns its pathname. Used as a context manager, it removes the directory and its contents when the with block exits.

This gives the scratch data an explicit lifetime. Code inside the block may create ordinary files with familiar pathlib or open operations, while cleanup belongs to the directory context rather than to every individual success path.

That ownership rule matters when an exception occurs. Python still exits the context manager, so normal cleanup is attempted even when processing fails halfway through.

Why inventing a temporary filename is unsafe

A tempting implementation is to generate a plausible path and open it later:

# Do not use this as a temporary-file creation strategy.
path = "/tmp/report.tmp"

if not Path(path).exists():
    with open(path, "w", encoding="utf-8") as file:
        file.write("result")

The check and creation are separate filesystem operations. Another process can create or replace that path between them. This is a classic time-of-check/time-of-use race.

Adding randomness yourself does not fix the underlying design unless creation is also performed atomically with the required exclusivity guarantees.

The tempfile APIs combine name selection with creation. The standard library documentation describes mkstemp() as creating a file without a creation race when the platform correctly implements the exclusive-create flag it relies on. Higher-level temporary-file APIs use the same secure creation rules.

The important guarantee is therefore not that a generated name “looks random.” It is that the library creates the temporary object using an operation designed for this purpose instead of handing your program an unchecked candidate name to create later.

Use TemporaryFile when only your code needs the file object

If the task needs scratch storage but no other component needs to reopen it by pathname, TemporaryFile keeps the interface small:

from tempfile import TemporaryFile

with TemporaryFile() as file:
    file.write(b"header\npayload\n")
    file.seek(0)
    data = file.read()

print(data)

The default mode is binary read/write mode. After writing, the file position is at the end, so seek(0) is required before reading those bytes back from the beginning.

The file is removed when it is closed. The context manager makes that close deterministic at block exit.

Do not build logic around TemporaryFile().name being a reusable pathname. The documentation explicitly warns that filesystem-name visibility differs by platform. On Unix-like systems the directory entry may never be created or may be removed immediately; other platforms behave differently.

If a path is part of the contract, choose an API that guarantees a visible name.

Use NamedTemporaryFile when another API needs a path

Some tools accept only a filename. NamedTemporaryFile creates a temporary file whose filesystem name is guaranteed to be visible and exposes that name through .name:

from tempfile import NamedTemporaryFile

with NamedTemporaryFile(mode="w", encoding="utf-8", suffix=".txt") as file:
    file.write("temporary report\n")
    file.flush()

    consume_path(file.name)

The flush() is significant. Python file objects buffer writes. If consume_path opens the path independently, flushing makes data already written through this file object available to the operating system before the consumer reads it.

Flushing is not the same as requesting durable storage after a crash or power failure. Temporary handoff usually needs visibility to another reader, not durability, so do not describe flush() as a persistence guarantee.

Reopening a named temporary file is platform-sensitive

A visible name does not mean every operating system permits the same reopen pattern while the original handle remains open. In particular, Windows file-sharing and deletion rules make some combinations behave differently from POSIX systems.

For portable code, keep the lifetime contract simple. If an external library can consume an already-open file object, prefer that. If it requires a path, understand whether it opens the file while your temporary-file handle is still open and choose the deletion policy supported by your Python version and target platforms.

Avoid tutorials that assume a reopen sequence works everywhere merely because it worked on one Unix machine.

Prefer automatic cleanup when the lifetime is lexical

A lexical lifetime means the resource is needed only inside a clear block of code. Context managers fit that shape well:

from tempfile import TemporaryDirectory

with TemporaryDirectory() as temp_dir:
    run_conversion(temp_dir)

Once run_conversion returns—or raises—the block ends and cleanup is attempted.

This is easier to reason about than creating a path in one function and hoping a distant caller eventually deletes it. The code that acquires the temporary resource visibly owns its lifetime.

Automatic cleanup is not a promise that deletion can never fail. Filesystem permissions, open handles, process termination, or platform behavior can interfere. For example, abrupt process termination cannot execute ordinary Python cleanup code. Design temporary data as disposable rather than relying on cleanup as a transactional guarantee.

Use mkstemp only when you need its lower-level contract

mkstemp() returns a raw operating-system file descriptor and a pathname:

import os
from tempfile import mkstemp

fd, path = mkstemp(suffix=".txt")

try:
    with os.fdopen(fd, "w", encoding="utf-8") as file:
        file.write("generated data\n")
    fd = -1

    consume_path(path)
finally:
    if fd != -1:
        os.close(fd)
    try:
        os.unlink(path)
    except FileNotFoundError:
        pass

This is intentionally more work. Unlike the high-level context-managed APIs, mkstemp() makes the caller responsible for both closing the descriptor and deleting the file.

The example transfers descriptor ownership to os.fdopen(). Once the with block exits successfully, that file object has closed the descriptor, so the sentinel prevents a second os.close() in finally. If constructing or using the file object fails before ownership is safely finished, the cleanup path still has enough information to close the original descriptor.

Most application code does not need this complexity. Use mkstemp() when a low-level descriptor or a manually controlled lifetime is genuinely part of the integration, not merely because it returns a convenient path.

Do not use mktemp to reserve a name

Python still documents the historical tempfile.mktemp() function, but it is deprecated because it returns a name without creating the file atomically.

That recreates the race we want to avoid:

process A asks for an unused-looking name
process A receives /tmp/example123
process B creates /tmp/example123
process A opens /tmp/example123 believing it owns the path

The gap between “choose a name” and “create the object” is the problem.

Use NamedTemporaryFile, TemporaryDirectory, mkstemp, or mkdtemp according to the resource and lifetime you actually need. Do not treat temporary-name generation as a separate reservation step.

TemporaryDirectory is useful for tests because it isolates names

Tests often need filesystem state but should not depend on a developer’s working directory:

from pathlib import Path
from tempfile import TemporaryDirectory


def load_config(path: Path) -> str:
    return path.read_text(encoding="utf-8").strip()


with TemporaryDirectory() as temp_dir:
    config_path = Path(temp_dir) / "config.txt"
    config_path.write_text("development\n", encoding="utf-8")

    assert load_config(config_path) == "development"

The test controls its own directory and filename. A second test can create the same relative filename in another temporary directory without colliding with the first one.

This isolation is useful, but it does not make tests independent of filesystem semantics. Permissions, case sensitivity, path-length limits, symlink behavior, and rename rules can still vary across platforms and filesystems. If your production behavior depends on one of those properties, test that property on the relevant environment rather than assuming a temporary directory emulates every filesystem.

Put temporary data on the right filesystem when operations depend on it

By default, tempfile chooses a platform-dependent temporary location. Environment variables can influence that choice.

Usually that is exactly what scratch data needs. But location matters when a later filesystem operation has locality requirements.

For example, code sometimes writes a replacement file and then renames it over a destination. If the intended atomic replacement operation requires source and destination to be on the same filesystem, creating the temporary file in the system-wide temporary directory may put it on a different filesystem.

In that case, create the temporary file in the destination directory:

from pathlib import Path
from tempfile import NamedTemporaryFile


def write_replacement(destination: Path, data: str) -> None:
    with NamedTemporaryFile(
        mode="w",
        encoding="utf-8",
        dir=destination.parent,
        prefix=f".{destination.name}.",
        suffix=".tmp",
        delete=False,
    ) as file:
        file.write(data)
        temporary_path = Path(file.name)

    try:
        temporary_path.replace(destination)
    except BaseException:
        temporary_path.unlink(missing_ok=True)
        raise

The purpose of dir=destination.parent is not performance folklore. It places the temporary entry beside the destination so a later replacement is not accidentally turned into a cross-filesystem operation.

This small example is not a complete crash-durable file-update protocol. Durability can require flushing file contents and directory metadata according to operating-system and filesystem semantics. Keep the distinction clear: temporary-file placement solves the pathname/filesystem relationship; it does not by itself guarantee persistence through a sudden machine failure.

Cleanup policy changes when ownership crosses a process boundary

A temporary resource is easy to clean up when one function owns it. Ownership becomes harder when a child process, background worker, or external application needs the path after the creating scope returns.

Consider this sequence:

create temporary path
start asynchronous consumer
leave context and delete temporary path
consumer tries to open path

The producer cleaned up correctly from its own perspective, but too early for the consumer.

The solution is not to disable cleanup casually. Define who owns the resource after handoff. The creator can wait until the consumer finishes, or ownership can move to a component that has an explicit cleanup step. A path that escapes its context manager needs a lifetime that escapes it too.

This is the same resource-management principle used for sockets, locks, and database transactions: lifetime should follow ownership, not convenience.

Temporary storage still consumes real resources

“Temporary” describes intended lifetime, not storage cost.

A temporary file can fill a filesystem. A temporary directory can accumulate millions of entries. An abandoned file can survive until an administrator or operating-system policy removes it. Creating scratch data in memory-backed temporary storage can consume memory or swap instead of ordinary disk capacity, depending on the environment.

If input size is untrusted or naturally large, enforce application-level limits. Cleanup after success does not protect a running process from exhausting storage before it reaches the cleanup point.

Also avoid assuming the default temporary directory is private merely because your filename is hard to guess. Use secure creation APIs, set application-appropriate permissions where needed, and do not place secrets in filenames. Filenames can appear in process diagnostics, logs, crash reports, or monitoring systems even when file contents are protected.

Common mistakes

Creating a predictable path with open

A fixed or guessed filename can collide with another process and may introduce a race. Let tempfile create the object rather than inventing a candidate path.

Returning a path from inside TemporaryDirectory

The directory is removed when the context exits. A returned pathname is therefore usually already invalid. Return the processed result, extend the directory lifetime, or transfer ownership explicitly.

Forgetting that mkstemp returns an open descriptor

Deleting the pathname is not a substitute for closing the descriptor. Treat descriptor closure and directory-entry removal as separate responsibilities.

Assuming flush means durable

flush() moves Python-buffered data toward the operating system so another opener can observe it. Crash durability is a stronger requirement with additional platform-specific steps.

Depending on TemporaryFile having a useful pathname

Its name visibility is deliberately platform-dependent. Choose NamedTemporaryFile when a visible filesystem path is required.

Making cleanup depend only on the happy path

Use context managers or try/finally when the resource must be released after exceptions as well as successful execution.

When each API is a good fit

Use TemporaryFile when your code needs file-like scratch storage and no pathname contract is required.

Use NamedTemporaryFile when you need a real temporary file and another API needs its visible name. Pay attention to reopen and deletion semantics on every supported platform.

Use TemporaryDirectory when a task needs a small workspace or several related paths. It is particularly convenient for tests and multi-file transformations.

Use mkstemp or mkdtemp when you specifically need lower-level ownership and are prepared to close and remove resources yourself.

If you only need an in-memory buffer, a temporary filesystem object may be unnecessary. io.BytesIO or io.StringIO can be simpler when data sizes are bounded and no API requires a file descriptor or path.

Conclusion

Safe temporary storage is mostly an ownership problem. Let the standard library create names and filesystem objects securely, then make the lifetime explicit.

Start with a context-managed TemporaryFile or TemporaryDirectory. Choose NamedTemporaryFile when a visible path is part of the interface, and drop to mkstemp only when its raw descriptor and manual lifetime are useful. Keep cleanup, cross-process ownership, filesystem placement, and resource limits visible in the design rather than treating temporary files as ordinary files that happen to live under /tmp.