Python essentials for engineers

Learning objectives
  • Decide when a task has outgrown Bash
  • Structure Python code as a reusable module with a main() entry point
  • Handle paths with pathlib
  • Parse options with argparse
  • Write small functions with type hints
  • Raise, catch and report exceptions so the script returns a meaningful exit status
1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.

Explanation

When to move from Bash to Python

Bash is ideal for running tools in sequence. Python is the better choice once a script needs data structures, careful parsing, arithmetic, reports or tests of its own.

Task Better fit
Run iverilog then vvp and stop on failure Bash or Make
Count PASS lines in one log grep -c
Parse 500 logs, group failures by error message, write JSON Python
Generate a register header from a CSV specification Python
Retry a flaky license-free tool run with a timeout Python

Scripts and modules

Every .py file is a module. When Python runs a file directly, its __name__ is '__main__'; when another file imports it, __name__ is the module name. Guarding the entry point keeps imports free of side effects, so a regression runner can import filelist and reuse its functions without running its command-line code.

def main() -> int:
    ...
    return 0

if __name__ == '__main__':
    raise SystemExit(main())

raise SystemExit(main()) turns the return value into the process exit status.

Paths with pathlib

pathlib.Path replaces string joining and most os.path calls.

Operation pathlib
Join Path('rtl') / 'fifo.v'
Name parts p.name, p.stem, p.suffix, p.parent
Test p.exists(), p.is_file(), p.is_dir()
Find files Path('rtl').glob('*.v'), rglob('*.sv')
Read and write p.read_text(), p.write_text(text)
Make a folder p.mkdir(parents=True, exist_ok=True)

Paths with spaces need no quoting because a Path is never split by a shell.

argparse

argparse builds --help, validates types and reports usage errors with exit status 2 automatically.

parser = argparse.ArgumentParser(description='Build a simulator file list.')
parser.add_argument('roots', nargs='+', type=Path, help='folders to search')
parser.add_argument('-o', '--output', type=Path, default=Path('files.f'))
parser.add_argument('--ext', action='append', default=None)
args = parser.parse_args()

Exceptions

Raise an exception when a function cannot do its job; catch it where you can do something useful, usually in main(). Catch specific types such as FileNotFoundError or ValueError, never a bare except: that would also hide typing mistakes. Print errors to sys.stderr and return a non-zero status.

Worked example

make_filelist.py collects HDL sources into a simulator file list (.f file) that iverilog -c files.f or verilator -f files.f can read. Packages must compile before the modules that import them, so files ending in _pkg.sv are listed first.

#!/usr/bin/env python3
"""Write a simulator file list from one or more source folders."""
import argparse
import sys
from pathlib import Path

DEFAULT_EXTENSIONS = ('.v', '.sv')


def find_sources(roots: list[Path], extensions: tuple[str, ...]) -> list[Path]:
    files: list[Path] = []
    for root in roots:
        if not root.is_dir():
            raise FileNotFoundError(f'not a folder: {root}')
        files.extend(p for p in root.rglob('*') if p.suffix in extensions and p.is_file())
    if not files:
        raise ValueError('no HDL sources found')
    # Packages first, then everything else in a stable order.
    return sorted(set(files), key=lambda p: (not p.stem.endswith('_pkg'), str(p)))


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('roots', nargs='+', type=Path)
    parser.add_argument('-o', '--output', type=Path, default=Path('files.f'))
    parser.add_argument('--ext', action='append', help='extension to include, e.g. .svh')
    args = parser.parse_args(argv)

    extensions = tuple(args.ext) if args.ext else DEFAULT_EXTENSIONS
    try:
        sources = find_sources(args.roots, extensions)
    except (FileNotFoundError, ValueError) as exc:
        print(f'make_filelist: {exc}', file=sys.stderr)
        return 1

    args.output.write_text(''.join(f'{p}\n' for p in sources))
    print(f'wrote {len(sources)} files to {args.output}')
    return 0


if __name__ == '__main__':
    raise SystemExit(main())

Run it:

python3 make_filelist.py rtl tb -o build/files.f
iverilog -g2012 -s tb_fifo -o build/sim.vvp -c build/files.f
python3 make_filelist.py missing; echo "status=$?"

main() accepts an optional argv list, so a test can call main(['rtl', '-o', 'out.f']) without touching sys.argv. Sorting gives the same file order on every machine, which keeps compile logs comparable. The list[Path] and list[str] | None hints need Python 3.10 or newer.

Check yourself

What does if __name__ == '__main__': protect against when another script imports this file? Why should file lists be sorted? Which exceptions does main() catch, and why not catch every exception? What exit status does argparse use for a usage error?

Engineering practice

Practice: Python essentials for engineers

Task→Run / reason→Check result→Explain why

Add an --exclude PATTERN option (repeatable) that skips any path matching a glob such as '*_old.v'; use Path.match. Then add a check that fails with status 1 when two files in the list have the same file name in different folders, printing both paths. Write two quick tests that call main() directly with a temporary folder.

Practice work is non-certified and is not stored by ASIC Campus.