Temporary Files

Creating scratch files and directories safely with the tempfile module — automatic naming, automatic cleanup, and the security pitfalls of doing it yourself.

Programs regularly need short-lived files: a scratch space for intermediate results, a staging file before an atomic rename, or an isolated directory for a test run. The tempfile module creates these safely — with unpredictable names, correct permissions, and automatic cleanup — solving problems that a naive hand-rolled "just use a fixed filename in /tmp" approach gets wrong in security-relevant ways.

Why Not Just Pick a Filename Yourself

A tempting shortcut is open("/tmp/scratch.txt", "w"). This has two real problems: the name is predictable, so a malicious or merely careless process could pre-create that file (e.g. as a symlink to something sensitive) before you open it — a classic temp-file race-condition vulnerability; and it isn't unique, so two runs of your program (or two users on a shared machine) can collide and corrupt each other's data. tempfile solves both by generating unpredictable names and creating the file atomically and exclusively (O_EXCL-style semantics).

Basic Syntax: NamedTemporaryFile

tempfile.NamedTemporaryFile() creates a temporary file, opens it, and returns a file-like object with a visible .name attribute giving its path. By default it's deleted automatically when closed.

On Windows, a file opened by NamedTemporaryFile with delete=True generally cannot be reopened by name while still open in the creating process — a platform difference worth knowing if your code needs to hand the path to another tool while still writing to it.

import tempfile

with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=True) as tmp:
    tmp.write("scratch data\n")
    tmp.flush()
    print("temp file at:", tmp.name)
    # ... pass tmp.name to another process/library if needed ...
# file is closed and deleted automatically here

TemporaryFile (No Visible Name)

If you don't need another process or API to open the file by path — you're just using it as scratch space within your own program — tempfile.TemporaryFile() is simpler and, on POSIX systems, more secure: the file is unlinked from the filesystem immediately after creation, so it has no discoverable name at all, yet remains usable via the returned file object until closed.

import tempfile

with tempfile.TemporaryFile(mode="w+b") as tmp:
    tmp.write(b"binary scratch data")
    tmp.seek(0)
    data = tmp.read()
# automatically cleaned up on close

Temporary Directories

tempfile.TemporaryDirectory() creates a uniquely-named directory and recursively removes it (and everything inside it) when the context manager exits — useful for staging multiple related scratch files or extracting an archive to inspect its contents.

import tempfile
import os

with tempfile.TemporaryDirectory() as tmpdir:
    print("working in:", tmpdir)
    scratch_path = os.path.join(tmpdir, "intermediate.json")
    with open(scratch_path, "w") as f:
        f.write("{}")
    # ... do work with files inside tmpdir ...
# tmpdir and everything in it is deleted here, recursively

Low-Level Functions: mkstemp and mkdtemp

tempfile.mkstemp() and tempfile.mkdtemp() are lower-level, non-automatic equivalents: they create the file/directory securely and return a raw file descriptor + path (for mkstemp) or just a path (for mkdtemp), but leave cleanup entirely to you. Use these only when you specifically need manual control over the file's lifetime (e.g. it must outlive the current function) — otherwise the context-manager versions above are safer defaults.

With mkstemp, the OS-level file descriptor fd must be closed exactly once — os.fdopen(fd, ...) wraps it in a normal file object so a with block closes it, but if you forget to close it (directly or via fdopen) it leaks for the life of the process.

import tempfile
import os

fd, path = tempfile.mkstemp(suffix=".log")
try:
    with os.fdopen(fd, "w") as f:   # fdopen takes ownership of fd
        f.write("manual cleanup required\n")
    # ... use path ...
finally:
    os.remove(path)   # you are responsible for deleting it

Controlling Location, Prefix, and Suffix

All the creation functions accept prefix, suffix, and dir keyword arguments to control the generated name and where it's placed. suffix is particularly useful for giving the file a recognizable extension (e.g. so another tool that dispatches on extension can process it).

import tempfile

with tempfile.NamedTemporaryFile(
    prefix="report_", suffix=".csv", dir="/var/tmp", delete=True
) as tmp:
    print(tmp.name)   # e.g. /var/tmp/report_a1b2c3.csv

Where Temp Files Are Created by Default

Without an explicit dir, tempfile picks a platform-appropriate default location by checking, in order, the TMPDIR, TEMP, and TMP environment variables, then falling back to a platform default (/tmp on most POSIX systems). tempfile.gettempdir() returns the resolved directory being used, which is useful for logging or diagnostics.

import tempfile

print(tempfile.gettempdir())   # e.g. '/tmp'

Cleanup Semantics: delete vs delete_on_close

By default, NamedTemporaryFile(delete=True) removes the file as soon as it's closed. Passing delete=False keeps the file on disk after closing — necessary when you need to close it, hand its path to another process, and have that process open it, then clean it up yourself afterward. Python 3.12 added delete_on_close, letting you keep delete=True (so it's removed eventually) while controlling whether closing alone triggers deletion versus only exiting the with block.

import tempfile
import subprocess
import os

tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False)
try:
    tmp.write("data for external tool\n")
    tmp.close()                      # must close before another process reads it
    subprocess.run(["wc", "-l", tmp.name], check=True)
finally:
    os.remove(tmp.name)              # manual cleanup since delete=False

Cleanup and Exception Safety

The context-manager forms (NamedTemporaryFile, TemporaryFile, TemporaryDirectory used with with) guarantee cleanup even if an exception is raised inside the block, exactly like any other context manager. This is the main reason to prefer them over mkstemp/mkdtemp plus manual try/finally bookkeeping whenever the automatic lifetime (tied to the with block) fits your use case.

import tempfile

with tempfile.TemporaryDirectory() as tmpdir:
    raise RuntimeError("something went wrong mid-processing")
# tmpdir is still deleted, even though an exception propagated out

Secure Temporary-File Practices

tempfile's creation functions already handle the core security properties for you: names are generated with enough randomness to be unguessable, and the underlying OS calls create the file exclusively (failing rather than silently reusing an existing file) with restrictive permissions (readable/writable only by the creating user, by default, on POSIX). The practices you're still responsible for: never construct a temp path yourself with os.path.join(tempfile.gettempdir(), "myfile") (this reintroduces the predictable-name problem tempfile exists to avoid), don't write sensitive data to delete=False files without cleaning them up in a finally block, and be aware that a shared multi-user /tmp means other local users may be able to see filenames (though not contents, given correct permissions) if you use mkstemp/mkdtemp with a predictable pattern.

Insecure (predictable, race-prone):
    path = "/tmp/myapp_scratch.txt"
    open(path, "w")

Secure (unguessable name, exclusive creation, correct permissions):
    tempfile.NamedTemporaryFile()

Key Takeaways

Never construct temp file paths yourself with a fixed or predictable name — use tempfile, which creates files with unguessable names and exclusive-creation semantics.

NamedTemporaryFile gives you a visible path (needed when another process/library must open the file); TemporaryFile has no discoverable name and is simpler when only your own code needs it.

TemporaryDirectory recursively creates and later removes a whole scratch directory tree.

mkstemp/mkdtemp are secure but manual — you own cleanup; prefer the context-manager forms unless you specifically need a longer-lived temp file.

Context-manager forms clean up automatically even when an exception is raised inside the block.

delete=False keeps the file after closing (for handing off to another process) but shifts cleanup responsibility to you — always remove it in a finally block.

path = "/tmp/myapp_scratch.txt" with open(path, "w") as f: f.write(data) import tempfile with tempfile.NamedTemporaryFile(mode="w", delete=True) as f: f.write(data)

A predictable, fixed temp path is vulnerable to race conditions and collisions between runs/users; tempfile generates an unguessable name and creates the file exclusively.

fd, path = tempfile.mkstemp() os.fdopen(fd, "w").write("data") # path never removed — leaks on every call fd, path = tempfile.mkstemp() try: with os.fdopen(fd, "w") as f: f.write("data") finally: os.remove(path) mkstemp does not clean up after itself; forgetting the try/finally around os.remove leaks temp files indefinitely, unlike the context-manager forms which clean up automatically.

tmp = tempfile.NamedTemporaryFile(mode="w", delete=False) tmp.write("data") tmp.close() subprocess.run(["tool", tmp.name]) # file left on disk forever tmp = tempfile.NamedTemporaryFile(mode="w", delete=False) try: tmp.write("data") tmp.close() subprocess.run(["tool", tmp.name]) finally: os.remove(tmp.name) delete=False disables automatic cleanup entirely; the caller must remove the file explicitly, ideally in a finally block so it happens even if the subprocess call raises.

What security problem does tempfile solve that a hardcoded '/tmp/scratch.txt' path does not?

A hardcoded path is predictable, letting another process pre-create it (potentially as a malicious symlink) before your code opens it, and it isn't unique across concurrent runs or users. tempfile generates unguessable names and creates files exclusively with restrictive permissions, avoiding both problems.

When would you choose TemporaryFile over NamedTemporaryFile?

When you only need the file as scratch space within your own program and never need another process or API to open it by path — TemporaryFile has no discoverable filesystem name (on POSIX it's unlinked immediately) and is simpler and marginally more secure.

What happens to a TemporaryDirectory's contents when its with block exits, including via an exception?

The directory and everything inside it is recursively deleted when the context manager exits, whether the block completed normally or an exception propagated out — cleanup is guaranteed either way.

Why does mkstemp() require more manual work than NamedTemporaryFile()?

mkstemp() is a low-level function that returns a raw file descriptor and path but performs no automatic cleanup or resource management — the caller must close the file descriptor (typically via os.fdopen) and remove the file explicitly, ideally in a try/finally block.

When would you pass delete=False to NamedTemporaryFile, and what responsibility does that shift to you?

When another process needs to open the file by path after your code has closed it (some platforms don't allow reopening a delete=True file by name while it's still open). It shifts cleanup responsibility to you — you must remove the file yourself, typically in a finally block.

How does tempfile decide which directory to create temp files in by default?

It checks the TMPDIR, TEMP, and TMP environment variables in order, and falls back to a platform-appropriate default (commonly /tmp on POSIX) if none are set; tempfile.gettempdir() returns the resolved directory.

Write a function that writes data to a temporary file, passes its path to a callback function, and guarantees the file is removed afterward even if the callback raises.

import tempfile def with_temp_file(data: bytes, callback): with tempfile.NamedTemporaryFile(mode="wb", delete=True) as tmp: tmp.write(data) tmp.flush() return callback(tmp.name)

Using NamedTemporaryFile as a context manager guarantees the file is deleted on exit from the with block, whether callback(tmp.name) returns normally or raises — cleanup does not depend on the caller remembering a finally block.

What happens when this code runs?

FileNotFoundError. The TemporaryDirectory's with block has already exited by the time open(tmp_path) is called outside it, so the directory (and note.txt inside it) has already been recursively deleted.

Write a function using tempfile.mkstemp that creates a secure temporary file, writes a string to it, and returns its path — leaving cleanup to the caller (document this in the function).

import tempfile import os def make_temp_with_content(content: str) -> str: """Creates a temp file with content and returns its path. Caller is responsible for os.remove(path) when done.""" fd, path = tempfile.mkstemp(suffix=".txt") with os.fdopen(fd, "w") as f: f.write(content) return path mkstemp returns a low-level descriptor and path with no automatic cleanup, which is appropriate here since the file must outlive this function call; os.fdopen(fd, 'w') wraps the descriptor so the with block closes it properly, and the docstring makes the caller's cleanup obligation explicit.

import tempfile
with tempfile.TemporaryDirectory() as d:
    tmp_path = d + "/note.txt"
    with open(tmp_path, "w") as f:
        f.write("hello")
print(open(tmp_path).read())