A script that starts with REPORT_DATE = "2026-05-01" hardcoded at the top,
edited by hand before each run, works fine for the person who wrote it and
breaks down the moment anyone else needs to run it — they have to open the
file, find the right line, and know what to change it to. A
command-line interface
turns that edit into an argument.
argparse: no dependency, built into Python#
The standard library's argparse
handles this with no extra dependency:
import argparse
def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Export orders for a date range.") parser.add_argument("--start-date", required=True) parser.add_argument("--end-date", required=True) parser.add_argument("--dry-run", action="store_true") return parserrequired=True makes --start-date mandatory — omitting it produces a
clear usage error instead of the script silently running with an undefined
value. action="store_true" turns --dry-run into a flag with no value to
supply, present or absent. argparse also generates --help output from
these declarations automatically, so the CLI documents its own options
without a separate written reference:
def main(): args = build_parser().parse_args() run_export(args.start_date, args.end_date, dry_run=args.dry_run)
if __name__ == "__main__": main()The __main__ guard exists so the module can be imported safely#
if __name__ == "__main__": runs main() only when the file is executed
directly — not when it is imported. This is what makes the export logic
testable: a test can import the module and call run_export(...)
directly with specific arguments, without triggering argument parsing or
the real side effects main() would otherwise cause on import.
Typer: less boilerplate, one more dependency#
Typer (built on Click)
derives the same CLI from function type hints instead of explicit
add_argument calls:
import typer
def main(start_date: str, end_date: str, dry_run: bool = False): run_export(start_date, end_date, dry_run=dry_run)
if __name__ == "__main__": typer.run(main)The parameter types and defaults are the CLI specification — dry_run: bool = False becomes an optional --dry-run flag with no extra
declaration. This scales especially well once a script grows into several
subcommands, which Typer supports directly; for one script with a handful
of flags, argparse's zero-dependency stdlib approach is often simpler to
justify.
CLI arguments are not the same thing as configuration#
Configuration and Secrets Management covers loading a database host or an API key from the environment, once, at startup. CLI arguments answer a different question: not "what does this pipeline connect to," but "what should this specific run do" — which date range, which mode, whether to actually write or just preview. Mixing the two is the source of a specific, recurring mistake:
A credential is not a CLI argument
Passing a password or API key as a command-line flag
(--db-password=hunter2) puts it in shell history and in the process list
every other process on the machine can read — a real information leak, not
a theoretical one. Credentials belong in the configuration layer covered in
Configuration and Secrets Management,
never as an argument the CLI itself accepts.
Common mistakes#
Business logic living directly under if __name__ == "__main__"#
Code written only inside the __main__ guard cannot be imported and called
from a test, or from another script, without going through argument parsing
first. Keep the guard thin — parse arguments, then call a plain function —
so the actual logic is reachable independent of the CLI layer.
No --help a teammate can discover on their own#
A script whose only documentation is its source code forces every new user
to read it before running it. argparse and Typer both generate --help
for free from the declarations already in the code — there is little reason
to skip it.
Treating every hardcoded value as worth exposing as a flag#
Not every constant needs to become a CLI argument. A value that never changes between runs — an internal timeout, a fixed batch size chosen once and rarely revisited — is often clearer left as a constant in the code than as one more flag nobody ever overrides in practice.
Further reading#
argparse— Python documentation and the Argparse Tutorial — the full standard-library reference and a worked walkthrough.- Typer and Click — the type-hint-driven alternative and the library it is built on.
- Command-line interface — the general concept this article's script wraps.