#!/usr/bin/env python3 # 1. Shebang: on macOS and Linux it lets you run the file as ./sample-script.py # after `chmod +x sample-script.py`. The Windows `py` command reads it too. """Text statistics: count the words and sentences in a text and estimate how long it takes to read. 2. Module docstring: the first string in the file. help() and IDEs show it. Free sample from 99helpers.com/file-types/py Run it: python3 sample-script.py (Windows: py sample-script.py) python3 sample-script.py notes.txt --top 5 """ # 3. Imports: standard library only, so it runs on any Python 3.10 or later # with nothing to install. One import per line, standard library first. import argparse import re import sys from collections import Counter from dataclasses import dataclass from pathlib import Path # 4. Constants: UPPER_CASE names by convention (PEP 8). Python has no real # constants; the name tells other programmers not to change the value. WORDS_PER_MINUTE = 200 # an assumed reading speed SAMPLE_TEXT = """\ A Python file is plain text. The interpreter reads it from top to bottom, compiles it to bytecode and runs it. Nothing runs until you ask it to! Does the file need a special editor? No: any text editor will do. """ # 5. A dataclass: a class whose __init__ and __repr__ Python writes for you, # from the type-annotated fields. frozen=True makes instances read-only. @dataclass(frozen=True) class Stats: words: int sentences: int top_words: list[tuple[str, int]] @property def minutes(self) -> float: return self.words / WORDS_PER_MINUTE # 6. A function with type hints and a default argument. Hints document the # types; Python itself doesn't enforce them, tools like mypy check them. def analyse(text: str, top: int = 3) -> Stats: words = re.findall(r"[^\W\d_]+(?:'[^\W\d_]+)?", text.lower()) sentences = len(re.findall(r"[.!?]+(?=\s|$)", text)) common = Counter(w for w in words if len(w) > 3).most_common(top) return Stats(words=len(words), sentences=sentences, top_words=common) # 7. Reading a file: always name the encoding. Before Python 3.15, open() # without one used the system's locale encoding (often cp1252 on Windows). def read_text(path: Path) -> str: return path.read_text(encoding="utf-8") # 8. Command-line arguments: argparse also generates --help for free. def parse_args(argv: list[str] | None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Count words and sentences.") parser.add_argument("file", nargs="?", type=Path, help="a UTF-8 text file") parser.add_argument("--top", type=int, default=3, help="how many words to list") return parser.parse_args(argv) # 9. main(): turns problems into a message and an exit code (0 means success) # instead of a traceback. def main(argv: list[str] | None = None) -> int: args = parse_args(argv) if args.file is None: text, source = SAMPLE_TEXT, "built-in sample text" else: try: text, source = read_text(args.file), str(args.file) except FileNotFoundError: print(f"error: {args.file} not found", file=sys.stderr) return 1 except UnicodeDecodeError: print(f"error: {args.file} is not UTF-8 text", file=sys.stderr) return 1 stats = analyse(text, args.top) # 10. f-strings: expressions in {braces}, with format specs after a colon. print(f"Source: {source}") print(f"Words: {stats.words:,}") print(f"Sentences: {stats.sentences}") print(f"Reading: {stats.minutes:.1f} min at {WORDS_PER_MINUTE} words/min") for rank, (word, count) in enumerate(stats.top_words, start=1): print(f"Top word {rank}: {word!r} x {count}") return 0 # 11. The entry-point guard: this block runs when the file is executed # directly, but not when another file imports it as a module. if __name__ == "__main__": sys.exit(main())