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.
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.
getattr()fn = getattr(runtime, "execute_line")
Also fully resolvable — PragyaLint treats this exactly like
runtime.execute_line, a normal attribute access.
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.
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.
--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
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.
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.