Dynamic dispatch

Static analysis sees your code as text and syntax — it has no idea what happens at runtime. Code that calls functions by name instead of by direct reference is invisible to a naive "is this name referenced anywhere" check. PragyaLint specifically detects the common shapes of this and adjusts its confidence accordingly, rather than confidently deleting something that's actually alive.

Patterns PragyaLint recognizes

Dispatch tables

A dict, list, tuple, or set literal whose values are bare names:

DISPATCH = {"echo": execute_line, "run": run_block}
HANDLERS = [handler_a, handler_b]

Names used this way are treated as used, project-wide — no downgrade needed, because this pattern is fully resolvable statically.

Literal getattr()

fn = getattr(runtime, "execute_line")

Also fully resolvable — PragyaLint treats this exactly like runtime.execute_line, a normal attribute access.

Computed getattr(), exec, eval, globals()/locals()

fn = getattr(runtime, user_input)   # name isn't a literal — can't resolve
exec(f"{cmd}()")
globals()[cmd_name]()

These are genuinely undecidable by static analysis — the actual name being looked up depends on runtime data. When PragyaLint detects any of these anywhere in the project, it downgrades every remaining unused_export/unused_local finding to low confidence, project-wide.

Why project-wide, not just the file with the call? A reflective call in app.py can just as easily reach a function defined in runtime.py. Limiting the safety check to only the file containing the getattr call would miss exactly the cross-file case that matters most — an interpreter's dispatch logic living in one file, calling functions defined in another.

The --force gate

Confidence downgrade alone isn't the only safety net. Once dynamic dispatch is detected anywhere in the project, --fix refuses to delete any definition — even at --confidence all — unless you also pass --force:

# Reports the finding, but does not delete anything:
pragyalint --fix exports --confidence all

# Explicitly overrides the safety check:
pragyalint --fix exports --confidence all --force

Manual override: the keep pragma

For a specific definition you know is used dynamically in a way PragyaLint can't detect (a computed name, a decorator-based registry), mark it directly:

def execute_line(line):  # pragyalint: keep
    ...

This exempts the definition from unused_export and unused_local entirely, regardless of confidence or dynamic-dispatch state elsewhere in the project.

What this doesn't solve

PragyaLint can't prove a computed getattr name is safe to delete — that's genuinely undecidable in general. The LOW-confidence downgrade plus the --force requirement exist precisely because of this limit: they make deletion opt-in rather than automatic once the analysis can no longer be fully certain, rather than pretending certainty it doesn't have.