Home Tutorials Standard Library

Standard Library

Python's sys Module: Paths, Exit Codes, and the Module Cache

Pyford Notes July 6, 2026 8 min read
Key points
  • sys.path is the ordered list of directories Python searches for modules; import fails with ModuleNotFoundError if nothing on it matches.
  • sys.modules is the actual cache backing every import statement -- checking it directly shows exactly what's already loaded and where from.
  • sys.exit(code) raises SystemExit, which unwinds normally through finally blocks, and sets the process's real exit status for shell scripts to check.
  • sys.stdout, sys.stderr, and sys.stdin are ordinary file-like objects, and reassigning them is a legitimate way to redirect a program's output temporarily.

sys.path: where import actually looks

sys.path is an ordinary list of directory strings, searched in order whenever an import statement runs. It starts with the running script's directory (or an empty string for the interactive interpreter), followed by PYTHONPATH entries, then the standard library and site-packages locations:

import sys

for p in sys.path:
    print(p)

# Adding a directory at runtime, most recent additions checked first if inserted at 0
sys.path.insert(0, "/opt/my_project/lib")
import my_custom_module   # now findable, because its directory is on sys.path

Every ModuleNotFoundError ultimately comes down to the target module's directory not being anywhere in this list. Mutating sys.path directly, as above, is a quick fix for scripts and one-off tooling; for anything meant to be installed and distributed, a proper package structure and pip install -e . is the more maintainable equivalent.

sys.modules: the import cache you can inspect

sys.modules is a dict mapping every already-imported module's dotted name to its module object — this is the actual cache that makes a second import json anywhere in a program cheap, since it's a dict lookup rather than re-reading and re-executing the file:

import sys, json

print("json" in sys.modules)   # True, once imported anywhere in the process
print(sys.modules["json"] is json)   # True -- same object, not a copy

# Forcibly removing a module from the cache (rarely needed, mostly for testing)
del sys.modules["json"]
import json   # re-executes json's top-level code, since the cache entry is gone

Test suites occasionally delete entries from sys.modules to force a clean re-import of a module between test cases, particularly when the module has import-time side effects that need to run fresh each time.

sys.exit(): setting a real process exit code

sys.exit(code) raises SystemExit, which propagates like any other exception — running finally blocks and context manager cleanup on its way out — and, if uncaught, terminates the interpreter with code as the process's exit status:

import sys

def main():
    if len(sys.argv) < 2:
        print("usage: script.py ", file=sys.stderr)
        sys.exit(1)     # shell sees exit code 1: something went wrong
    # ... do the actual work ...
    sys.exit(0)          # explicit success, though returning normally also exits 0

if __name__ == "__main__":
    main()

Shell scripts and CI pipelines check this exit code to decide whether a step succeeded; returning a non-zero code on failure is what lets && chains and if my_script.py; then ... constructs work correctly. Because SystemExit is just an exception, a bare except: clause elsewhere in the program can accidentally swallow it and prevent the program from actually exiting — another good reason to always catch specific exception types rather than everything.

sys.argv, stdin, stdout, and stderr

sys.argv is the raw list of command-line arguments, with sys.argv[0] being the script name itself and the rest being whatever was typed after it — unparsed and unvalidated, which is why real command-line tools build argparse on top of it rather than reading it directly. sys.stdout, sys.stderr, and sys.stdin are ordinary file-like objects that print(), error tracebacks, and input() use by default, and reassigning them temporarily is a standard way to capture or redirect output:

import sys, io

captured = io.StringIO()
old_stdout = sys.stdout
sys.stdout = captured
try:
    print("this goes into the StringIO buffer, not the terminal")
finally:
    sys.stdout = old_stdout   # always restore it, even if something raised

print(captured.getvalue())   # prints the captured text now, to the real stdout

Runtime introspection: version, platform, recursion limit

sys.version_info gives a comparable tuple for feature-gating code across Python versions, sys.platform distinguishes "linux", "darwin", and "win32" for OS-specific branches, and sys.getrecursionlimit() / sys.setrecursionlimit() control how deep recursive calls can go before Python raises RecursionError to protect the C call stack underneath the interpreter:

import sys

if sys.version_info >= (3, 10):
    pass   # safe to use match statements, added in 3.10

print(sys.platform)             # "linux", "darwin", "win32", ...
print(sys.getrecursionlimit())   # 1000 by default

The complete reference for every attribute and function sys exposes is in the sys module documentation.