{"record":{"id":"a37174143e33f5fe","repo":"pytest-dev/pytest","slug":"cannot-create-lockfile-in-p","errorCode":null,"errorMessage":"cannot create lockfile in {p}","messagePattern":"cannot create lockfile in (.+?)","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"src/_pytest/pathlib.py","lineNumber":295,"sourceCode":"        except Exception:\n            pass\n        else:\n            _force_symlink(root, prefix + \"current\", new_path)\n            return new_path\n    else:\n        raise OSError(\n            \"could not create numbered dir with prefix \"\n            f\"{prefix} in {root} after 10 tries\"\n        )\n\n\ndef create_cleanup_lock(p: Path) -> Path:\n    \"\"\"Create a lock to prevent premature directory cleanup.\"\"\"\n    lock_path = get_lock_path(p)\n    try:\n        fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)\n    except FileExistsError as e:\n        raise OSError(f\"cannot create lockfile in {p}\") from e\n    else:\n        pid = os.getpid()\n        spid = str(pid).encode()\n        os.write(fd, spid)\n        os.close(fd)\n        if not lock_path.is_file():\n            raise OSError(\"lock path got renamed after successful creation\")\n        return lock_path\n\n\ndef register_cleanup_lock_removal(lock_path: Path, register: Any) -> Any:\n    \"\"\"Register a cleanup function for removing a lock.\"\"\"\n    pid = os.getpid()\n\n    def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:\n        current_pid = os.getpid()\n        if current_pid != original_pid:\n            # fork","sourceCodeStart":277,"sourceCodeEnd":313,"githubUrl":"https://github.com/pytest-dev/pytest/blob/0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc/src/_pytest/pathlib.py#L277-L313","documentation":"Raised by create_cleanup_lock when os.open with O_EXCL fails because the lock file already exists. The lock (a '.lock' sibling to a directory, line 291-295) prevents concurrent cleanup of numbered tmp dirs; an existing lock means another process holds it. The original FileExistsError is chained via 'from e' and re-raised as OSError.","triggerScenarios":"Two pytest runs concurrently cleaning the same tmp dir tree; a previous crashed pytest leaving a stale lock file; manual creation of a '.lock' file in the tmp root.","commonSituations":"Stale lock files left by killed pytest processes; parallel CI jobs sharing a basetemp; PID reuse making cleanup_on_exit skip removal.","solutions":["Remove the stale lock file manually: rm <dir>.lock (after confirming no live process holds it).","Use separate basetemp directories per concurrent run (--basetemp).","Run the cleanup lock removal or rely on pytest's atexit hooks to reap stale locks."],"exampleFix":"// before (stale lock blocks run)\nrm -rf /tmp/pytest-of-user/.lock  # may be unsafe if a run is live\n// after (avoid sharing)\npytest --basetemp=/tmp/pytest-run-A","handlingStrategy":"validation","validationCode":"lock = get_lock_path(p)\nif lock.exists():\n    # stale; remove only if no live process holds it\n    lock.unlink()","typeGuard":"def _no_stale_lock(p) -> bool:\n    return not get_lock_path(p).exists()","tryCatchPattern":"try:\n    create_cleanup_lock(p)\nexcept OSError:\n    # retry once or use a different basetemp\n    pass","preventionTips":["Give each concurrent pytest run its own basetemp.","Clean stale lock files from crashed runs before re-running."],"tags":["pytest","pathlib","oserror","locking","concurrency"],"backgroundTag":null,"analyzedSha":"0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc","analyzedAt":"2026-08-11T20:52:36.969Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}