Skip to content

← Parallel Python with Process Pools step 7 of 24

Hard Framework

Audit a Library for Import-Time Side Effects

The article listed calls that a library should not make when it is imported. Write a checker that finds them in a library's source without running it, so CI can check every change.

audit_library("import logging\n\nlogging.basicConfig()\n")
# [("L003", 3, "logging.basicConfig")]

What to write

def audit_library(source: str) -> list[tuple[str, int, str]]:

Parse source with ast.parse, and let a SyntaxError propagate. Never run the source. Return one (code, line, detail) tuple per finding:

Code Finds Where
L001 A call whose last name is set_start_method Every call in the source, including function bodies and annotations
L002 A call whose last name is ProcessPoolExecutor, ThreadPoolExecutor, InterpreterPoolExecutor or Pool Import-time code
L003 A call whose last name is basicConfig Import-time code
L004 A call to exactly signal.signal Import-time code
L005 A call to exactly os.environ.update, os.environ.setdefault or os.putenv, or an assignment to os.environ[...] Import-time code

Names

  • Build a call's dotted name from ast.Name and ast.Attribute nodes only. The last name of multiprocessing.set_start_method is set_start_method. A bare Pool() counts.
  • A call such as factory().Pool() has no dotted name, so it matches nothing.
  • detail is the call's full dotted name, or "os.environ" for an assignment.
  • Match names only. Don't follow imports, resolve aliases such as import os as o, or look inside a function to see what it calls.

Import-time code

Import-time code is everything in the source except these parts:

  • The bodies of functions, async functions and lambdas.
  • Annotations, type parameters such as the [T] in def first[T], and type alias statements. Python 3.14 does not evaluate them at import.

These parts count as the code around them, not as a body:

  • A function's decorators and default values, and a lambda's default values.
  • A class body, with the class's decorators, bases and keyword arguments. A class at the top level is import-time code, and a class inside a function is not.

Scan comprehensions and generator expressions like any other code. Every branch counts, including if __name__ == "__main__": and code that can never run.

Assignments to os.environ

  • Check plain, augmented and annotated assignments, such as os.environ["A"] += "1".
  • Look inside tuple, list and starred targets. Report one finding per os.environ[...] target, with the assignment statement's line number.
  • An annotated assignment with no value, such as os.environ["A"]: str, assigns nothing, so it is not an assignment finding. Still check the calls inside its target.
  • del os.environ["A"] and other methods such as os.environ.pop are not findings.

Output

  • Use the call's own line number for a call finding.
  • Sort the findings by line, then code, then detail.
  • Keep duplicates. Two calls that give the same tuple are two findings.

L001 applies even with force=True, because the library still overrides the application's choice.

Your submission must pass mypy --strict.