A Python script that works in an interactive terminal can fail before its first statement when a scheduler supplies a different path base. The file may still be present. A launcher can already be rooted in scripts, while a task adds scripts/worker.py again. The resulting duplicate directory points somewhere else entirely.

The useful question is not merely whether the file exists, but which component constructed the path that was checked. A second distinction matters just as much: a script that never ran has not reported an empty result. This guide addresses the informational search intent behind “Python scheduled script not found,” using an offline regression fixture rather than changes to an existing scheduler.

The source note documents duplicated path prefixes and the danger of interpreting missing execution as a zero count. The example below is newly written synthetic code. It does not reproduce task identifiers, historical incident counts, or claims about a particular environment being repaired.

Establish one owner for the path base

A relative script path needs an explicit base directory. When the launcher already owns the scripts directory, the task should not repeat that directory in its relative value. Record the base, configured value, and resulting target before deciding whether a missing-file error reflects incorrect resolution or a genuinely absent file.

Consider a disposable project containing only scripts/worker.py. Joining the scripts directory with worker.py selects the intended file. Joining that same directory with scripts/worker.py selects a different location: scripts/scripts/worker.py. Python has not moved or duplicated anything. Two configuration layers have independently assumed responsibility for the prefix.

Keep three concepts separate: the process working directory, the directory containing the script, and the base declared by the launcher. They may coincide during manual testing, but that coincidence is not a contract. An interactive command can succeed because the terminal happens to be in the project root. A scheduled invocation can fail because its base is already one level lower.

A practical diagnostic record therefore contains three fields: the configured base, the unexpanded relative value, and whether the resolved target is a file. Shared reports should use generic directory labels rather than full user-specific paths. There is no need to expose an entire environment to explain a repeated prefix.

Do not turn this diagnosis into a universal rewrite rule. If the resulting path is correct but the file is absent, removing a directory component is not a repair. Check the expected installation layout, filename, and availability of the relevant filesystem instead. The symptom “not found” does not, by itself, identify the cause.

Understand what pathlib does not repair

Path normalization follows filesystem semantics; it does not infer the intended application layout. A repeated directory name is legal and will not be removed merely because it looks redundant. Construct the target from a declared base, require successful strict resolution, and separately confirm that the existing target is a file.

The Path class uses the path semantics of the platform running Python. Its slash operator joins components, but a fully absolute right-hand component can replace the earlier base. Decide whether a configuration field accepts a relative value or an absolute location. Allowing that interpretation to drift between machines makes the configuration harder to review.

Calling resolve(strict=False) is not an existence check. It can return an absolute result while part of that result is missing. In particular, it will not transform scripts/scripts/worker.py into scripts/worker.py. Both are meaningful path structures, and a general-purpose library cannot decide which one the application designer wanted.

The fixture deliberately checks this behavior. The incorrect path is already absolute inside a temporary directory. Resolving it without strict checking leaves it pointing to the same incorrect location. That is the expected result, not a failure of normalization. A string that looks cleaner is not evidence that a runnable file has been located.

Strict resolution supplies a different guarantee: resolve(strict=True) raises FileNotFoundError for the missing target in this fixture. An existing target still needs is_file() because a directory is not the script being requested. Other filesystem errors should retain their own failure classification rather than being flattened into a generic missing-file message.

These checks support diagnosis, not a claim of permanent filesystem stability. A file can change after a check. Nor does normalization, by itself, establish a complete file-access isolation boundary. The example operates only on files it creates in a disposable local directory and does not accept arbitrary external paths.

Specify the interpreter, script, and working directory independently

A reliable launch identifies the interpreter, the absolute script target, and the child working directory separately. An absolute script path prevents working-directory changes from redirecting script lookup. It does not automatically fix relative data paths inside the script, which need their own explicit base and compatibility tests.

The fixture uses sys.executable to launch the current Python interpreter. It passes the absolute script path as a separate item in an argument list and keeps shell=False. There is no command string assembled for a shell to reinterpret. This makes the boundary between the executable, script, and option visible in code review.

The Python subprocess documentation notes platform differences in executable lookup. Setting cwd should not be treated as a universal repair for finding an unqualified interpreter name. The fixture avoids that ambiguity by using the current interpreter rather than searching for another executable. Readers still need to verify the interpreter version before running the test.

The cwd argument controls the child's working directory. That setting matters if application code reads data.json without specifying a base. An absolute script location only fixes which script is opened; it does not redefine every relative file access performed later. Treat script discovery and application resource discovery as separate contracts.

For resources intentionally stored beside a normal file-backed script, Path(__file__).resolve().parent can provide an explicit starting point. For resources intentionally selected from the caller's working directory, preserve that behavior and document it. Blindly moving every resource lookup beside the script simply replaces one hidden assumption with another.

This worker reads no external application data. Its only output is a small JSON object with a fixed marker, a completed state, and a synthetic zero count. Consequently, the test proves script selection and result handling under two working directories, not compatibility of an unrelated application's resource-loading behavior.

Verification: run three local regression checks

The following fixture creates its own temporary scripts directory and tests a duplicate prefix, two working directories, and the difference between an unknown result and a completed empty result. Any failed assertion is a failed verification. No existing scheduled task or external service is needed to reproduce the behavior.

Prerequisites: Python 3.11.15, a writable local temporary directory, and permission to start local child processes. No third-party packages, network access, or elevated privileges are required. Save the complete code below as paths_lab_20260921.py in your own sandbox folder. Run Python normally; do not enable an optimization option that disables assertions.

  1. Run python --version and require Python 3.11.15. Then run python paths_lab_20260921.py. Inspect check1: the duplicated target must be absent, the attempted launch must return a nonzero status, and the unresolved data count must remain null rather than becoming zero.
  2. Inspect check2 from that same command. Strict resolution must reject the missing target. The absolute target must run successfully from both temporary working directories and return identical JSON objects. An exception or a result difference fails this check, even if some text was printed.
  3. Inspect check3. A relative script name must fail when launched from the other directory. The successful launch must have the expected marker, completed state, and explicit zero count. Finally require passed=true and a zero exit status from the overall test process.
"""Offline path-resolution regression lab; only disposable temporary files are written."""
from pathlib import Path
from tempfile import TemporaryDirectory
import json
import platform
import subprocess
import sys


def worker():
    print(json.dumps({'marker': 'path-lab-v1', 'state': 'completed', 'count': 0}))


def launch(script, cwd):
    return subprocess.run([sys.executable, str(script), '--worker'], cwd=str(cwd),
                          capture_output=True, text=True, shell=False, timeout=20)


def main():
    if platform.python_version() != '3.11.15':
        raise RuntimeError('This fixture requires Python 3.11.15')
    with TemporaryDirectory(prefix='path-lab-') as folder:
        root = Path(folder).resolve()
        base = root / 'scripts'
        elsewhere = root / 'other'
        base.mkdir(); elsewhere.mkdir()
        target = base / 'worker.py'
        target.write_bytes(Path(__file__).read_bytes())
        wrong = base / 'scripts' / 'worker.py'
        assert target.is_file() and not wrong.exists()
        assert wrong.resolve(strict=False) == wrong
        bad = launch(wrong, base)
        assert bad.returncode != 0 and not bad.stdout.strip()
        check1 = {'duplicateMissing': True, 'badExitNonzero': True,
                  'unresolvedCount': None}
        try:
            wrong.resolve(strict=True)
        except FileNotFoundError:
            strict_rejected = True
        else:
            strict_rejected = False
        absolute = target.resolve(strict=True)
        first = launch(absolute, base)
        second = launch(absolute, elsewhere)
        first.check_returncode(); second.check_returncode()
        a = json.loads(first.stdout); b = json.loads(second.stdout)
        assert strict_rejected and a == b
        check2 = {'strictRejectedMissing': strict_rejected,
                  'cwdIndependent': a == b, 'successfulRuns': 2}
        relative_bad = launch(Path('worker.py'), elsewhere)
        assert relative_bad.returncode != 0
        assert a == {'marker': 'path-lab-v1', 'state': 'completed', 'count': 0}
        try:
            bad.check_returncode()
        except subprocess.CalledProcessError:
            exit_rejected = True
        else:
            exit_rejected = False
        assert exit_rejected
        check3 = {'relativeFailedElsewhere': True, 'nonzeroRejected': exit_rejected,
                  'completedCount': a['count'], 'markerVerified': True}
    print(json.dumps({'python': platform.python_version(), 'check1': check1,
                      'check2': check2, 'check3': check3, 'passed': True}, indent=2))


if __name__ == '__main__':
    if sys.argv[1:] == ['--worker']:
        worker()
    else:
        main()

This code was actually executed in the scheduler environment with Python 3.11.15. All three groups passed and the overall process returned zero. The observed values included check2.successfulRuns=2, check3.completedCount=0, and check1.unresolvedCount=null. These are measurements from the synthetic fixture, not evidence that a historical task in the source note has been repaired or that any external dataset is empty.

The fixture copies its own complete reviewed file into the temporary scripts directory. The child receives --worker, entering a branch that prints only the fixed JSON result. The temporary directory is cleaned up after the checks. Do not replace that disposable directory with an existing project directory; the isolation is part of the test design.

The failure checks intentionally avoid requiring a particular operating-system error string or one specific nonzero exit value. Those details can vary. What matters here is the absence of a worker result and an unsuccessful process status. This keeps the assertions focused on the contract being tested rather than incidental wording.

Do not collapse unknown execution into an empty result

A zero count is application data returned after a successful run, not a substitute for a launch failure. Require a completed process, a parseable payload, and the expected worker marker before interpreting the count. If any required evidence is missing, leave the result unknown and report the failure at its actual layer.

The incorrect launch in the fixture produces no worker output, so its count remains null. The correct launch returns state=completed, marker=path-lab-v1, and count=0. Both situations may show no data rows in a simplistic dashboard, but they describe different observations. One observed an empty synthetic result; the other never obtained a result.

A zero return code is a useful process signal, but it is not a complete output contract. A child might exit normally after printing help text. JSON parsing should reject that output. A different marker should fail the identity check. A nonzero status must fail check_returncode(), even if the error output happens to contain something that resembles a result.

The fixture combines these checks rather than choosing one. The correct worker must produce the exact expected object, and the failed launch must raise CalledProcessError when its return code is checked. This demonstrates the difference between process-level completion and application-level evidence without interacting with any real task state.

A larger application may also need an execution identifier and an output-format version. Those extensions belong in that application's own test contract. This example intentionally does not update external records or trigger downstream actions. Finding the script, completing the worker, and completing subsequent work are separate milestones; a report should say which milestone its evidence actually supports.

Adoption checklist, limitations, and official references

Adopt an explicit path contract rather than scattering machine-specific absolute strings throughout the code. Derive an absolute target from a configurable base, exercise it under different working directories, and preserve the version and observed results. The goal is a reviewable diagnosis, not an unsupported claim that one local test validates every scheduler.

  • Configuration ownership: Choose exactly which layer adds the directory prefix, and document whether task values are relative or absolute.
  • Preflight: Confirm the base is available and the selected target is an existing file. Stop the attempted launch when these checks fail.
  • Regression coverage: Keep the correct target, duplicated prefix, and alternative working directory as distinct cases rather than testing only the happy path.
  • Result validation: Check the exit status, parseable format, worker marker, and completed state together. Preserve the distinction between unknown and zero.
  • Evidence scope: Separate a passing local fixture from a verified launcher configuration and from completion of the actual application task.

There is no performance claim or benchmark in this approach. If the fixture fails, investigate the Python version, temporary-directory availability, selected target, and child-process result before weakening any assertion. An environment that does not allow local child processes does not meet the stated prerequisites; that restriction is not evidence of a pathlib defect.

The Python 3.11 pathlib documentation supports the path-joining, strict-resolution, and file-type distinctions. The Python 3.11 subprocess documentation supports argument sequences, working-directory configuration, return-code checks, and using the current interpreter. These are maintained branch documents, whereas the executed fixture is pinned to Python 3.11.15. The documentation branch and the tested patch release should not be described as an identical version snapshot.

First establish that the intended script was selected; then establish that it completed. That narrow sequence turns a vague scheduling symptom into testable evidence and prevents a missing observation from being presented as a normal empty result.