pytest-dev/pytest · error · ValueError
is not a normalized and relative path
Error message
{basename} is not a normalized and relative path What it means
TempPathFactory.mktemp() builds a subdirectory under basetemp from a caller-supplied basename. _ensure_relative_to_basetemp() normalizes the name then asserts that (basetemp/basename).resolve().parent == basetemp — i.e. the resolved path stays directly under basetemp. Absolute paths and any name that escapes via '..' fail this check, blocking path traversal and writes outside the temp tree.
Solutions
- Pass a plain relative name with no separators or `..` segments: 'foo', 'foo-', 'subdir'.
- Sanitize before calling: strip leading '/', reject any '..' component, prefer a single path component.
- If you need a nested layout, create the parent with mktemp first and join inside it yourself; do not push nesting into the basename.
Example fix
# before
sub = factory.mktemp(f"{user_supplied}/data") # user_supplied may be '../x'
# after
import re
safe = re.sub(r"[^A-Za-z0-9_.-]", "_", user_supplied) or "tmp"
sub = factory.mktemp(safe) Defensive patterns
Strategy: validation
Validate before calling
import os
def safe_basename(name: str) -> str:
"""Sanitize a caller-supplied name for TempPathFactory.mktemp()."""
# Strip any absolute prefix and reject traversal.
name = os.path.basename(os.path.normpath(name))
if not name or name in (os.curdir, os.pardir) or '..' in name.split(os.sep):
raise ValueError(f"unsafe basename for mktemp: {name!r}")
return name Type guard
import os
def is_safe_mktemp_basename(name: str, basetemp) -> bool:
norm = os.path.normpath(name)
return (
not os.path.isabs(norm)
and (basetemp / norm).resolve().parent == basetemp.resolve()
) Try / catch
from _pytest.tmpdir import TempPathFactory
def robust_mktemp(factory: TempPathFactory, name: str):
try:
return factory.mktemp(name)
except ValueError:
# Fall back to a sanitized single-component name.
import re
safe = re.sub(r"[^A-Za-z0-9_.-]", "_", name) or "tmp"
return factory.mktemp(safe) Prevention
- Never pass user input or test IDs straight into mktemp(); sanitize first.
- Use single path components (no '/', no '..') as basenames.
- If you need nested dirs, mktemp the parent and join children yourself inside the temp tree.
When it happens
Trigger: Calling factory.mktemp('/abs/path'), factory.mktemp('../escape'), factory.mktemp('a/../../b'), or any basename whose normpath+resolve no longer sits immediately under basetemp. Reachable from internal callers (the tmp_path_factory fixture, the tmpdir factory) or plugin code that invokes mktemp directly.
Common situations: A plugin derives the basename from user input or test IDs and forgets to sanitize; a test passes an absolute path expecting mktemp to honor it; refactoring code that used to join paths manually.
Related errors
- The temporary directory
- The temporary directory
- tmp_path_retention_count must be >= 0. Current input
- absolute tolerance can't be negative
- `allow_unwrapped=True` bypasses the `match` and `check`…
AI-assisted analysis of pytest-dev/pytest@0d6fbdeffa (2026-08-11).
Data as JSON: /api/errors/acd042c01c55dc1d.
Report an issue: GitHub.
Appendix: source
Thrown at src/_pytest/tmpdir.py:114
if count < 0:
raise ValueError(
f"tmp_path_retention_count must be >= 0. Current input: {count}."
)
policy: RetentionType = config.getini("tmp_path_retention_policy")
return cls(
given_basetemp=config.option.basetemp,
trace=config.trace.get("tmpdir"),
retention_count=count,
retention_policy=policy,
_ispytest=True,
)
def _ensure_relative_to_basetemp(self, basename: str) -> str:
basename = os.path.normpath(basename)
if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
raise ValueError(f"{basename} is not a normalized and relative path")
return basename
def mktemp(self, basename: str, numbered: bool = True) -> Path:
"""Create a new temporary directory managed by the factory.
:param basename:
Directory base name, must be a relative path.
:param numbered:
If ``True``, ensure the directory is unique by adding a numbered
suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
means that this function will create directories named ``"foo-0"``,
``"foo-1"``, ``"foo-2"`` and so on.
:returns:
The path to the new directory.
"""
basename = self._ensure_relative_to_basetemp(basename)View on GitHub (pinned to 0d6fbdeffa)