Python essentials for engineers
- 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
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?
Practice: Python essentials for engineers
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.