androidengineers.Book a session

Python 10 · Testing and project structure

Project metadata, reproducible setup, and CLI design

articleSelf-paced

Learn the concept

A reusable project needs a documented entry point, dependency declaration, and test command. A pyproject.toml file can describe project metadata and tooling, while a lock or pinned environment strategy improves reproducibility. Do not assume someone else has the same global packages installed.

A command-line interface should validate arguments and provide meaningful exit behavior. Use argparse for required values, help text, and type conversion rather than manually indexing sys.argv. Separate argument parsing from business logic.

Organize code by responsibility: parsing, domain transformations, adapters, and entry points. Keep sample data synthetic and small. A README should show a successful command and explain a failure case. Packaging is about making the software usable by another person, not only making it run on your laptop.

Run and inspect

import argparse

parser = argparse.ArgumentParser(description="Process document batches")
parser.add_argument("--limit", type=int, default=10)
args = parser.parse_args(["--limit", "3"])
assert args.limit == 3

Your exercise

Add a CLI to your document processor with input, output, and limit arguments. Test --help, a missing input, and an invalid limit.

Check your understanding

A fresh environment can follow the README and run the tests and one sample command.

Guided workshop: build a CLI that can be tested

Separate argument parsing from computation. Tests should call the computation directly; a small command wrapper handles strings, output, and exit status. The standard library is enough for this exercise.

Save this as summary.py. Running python3 summary.py 10 2 prints 8; running it with more rejected than received produces an argument error and a nonzero exit status.

# academy-check
import argparse

def accepted(total: int, rejected: int) -> int:
    if not 0 <= rejected <= total:
        raise ValueError("require 0 <= rejected <= total")
    return total - rejected

def main(argv=None):
    parser = argparse.ArgumentParser()
    parser.add_argument("total", type=int)
    parser.add_argument("rejected", type=int)
    args = parser.parse_args(argv)
    try:
        result = accepted(args.total, args.rejected)
    except ValueError as error:
        parser.error(str(error))
    print(result)

assert accepted(10, 2) == 8
if __name__ == "__main__":
    main(["10", "2"])  # Replace with main() to use terminal arguments.

The explicit argument list makes the demonstration deterministic. In the actual command entry point, use main() so argparse reads the terminal's arguments. The pure function remains reusable from an API, batch worker, or test.

Exercise: create test_summary.py using unittest. Cover zero inputs, all rejected, negative totals, and excessive rejections. Run python3 -m unittest discover -v. Keep tests in a fresh temporary directory when they touch files, and use a fake storage implementation when testing business logic alone.

Worked test: with self.assertRaises(ValueError): accepted(2, 3). For a successful case use self.assertEqual(accepted(2, 2), 0). A test that simply repeats the implementation's arithmetic misses the invalid-input contract.

Reproducibility checkpoint: record the Python version, setup command, test command, and sample invocation in a README. Create an isolated virtual environment even when this initial project has no third-party dependencies. Once dependencies exist, pin a tested resolution; a dependency name alone cannot reproduce an environment. A peer should be able to run the tests from a clean checkout without your editor configuration.

YOUR LEARNING JOURNEY

0 of 42 available lessons completed

Progress saved in this browser. No account needed.
Project metadata, reproducible setup, and CLI design | Python for AI Engineering | Android Engineers