Chapter 21 of 24 | Python
Safety, portability, and performance
Reliable Python code validates boundaries, handles failures explicitly, and measures performance before changing design. Dynamic typing does not remove the need for precise data contracts.
Validate untrusted input
Data from users, files, networks, environment variables, and third-party packages can be malformed or unexpectedly large. Validate type, range, encoding, and structure before using it. Avoid relying on assert for validation because optimized execution can disable assertions. Use explicit exceptions or result values for input errors.
def parse_port(value):
try:
port = int(value)
except (TypeError, ValueError) as error:
raise ValueError("port must be an integer") from error
if not 1 <= port <= 65535:
raise ValueError("port is outside the valid range")
return port
Catch specific expected exceptions and preserve their context with exception chaining. Do not silently substitute a default when it makes invalid data appear valid.
Security and safe interfaces
Do not build shell commands by concatenating untrusted text; use subprocess.run with an argument list and appropriate validation. Use parameterized queries for databases and context-aware escaping for HTML. Avoid unsafe deserialization of untrusted data, including Python pickle, which can execute code during loading. JSON is safer as a data format but still requires validation and resource limits.
Keep secrets out of source code and logs. Apply least privilege, update dependencies, and review package provenance. Safety includes predictable handling of errors and resource exhaustion, not just preventing crashes.
Measure and optimize
from time import perf_counter
start = perf_counter()
result = sum(number * number for number in range(1_000_000))
elapsed = perf_counter() - start
print(f"Computed in {elapsed:.3f}s")
Use cProfile to find CPU hotspots and tracemalloc to inspect Python allocation traces. Benchmark representative inputs, repeat measurements, and keep the result observable so work is not optimized away by the test setup. A faster microbenchmark may not improve the complete application.
Concurrency and the GIL
In standard CPython builds, the Global Interpreter Lock (GIL) historically limits simultaneous execution of Python bytecode by multiple threads, though threads remain useful for I/O concurrency. Processes can use multiple CPU cores at the cost of communication and startup overhead. Alternative runtimes and newer free-threaded CPython builds have different behavior and constraints. Do not assume one model applies to every Python implementation; measure your target runtime.
Portability and reproducibility
State supported Python versions and operating systems. Use pathlib rather than hard-coded path separators, specify file encodings, and avoid depending on dictionary ordering only when targeting obsolete Python versions. Test in clean environments and account for platform-specific filesystem and process behavior.
Beginner-to-master progression
- Beginner: validate input and report meaningful errors.
- Practitioner: use safe subprocess/database APIs, profile hotspots, and test multiple environments.
- Advanced: reason about threat models, resource exhaustion, interpreter/runtime differences, concurrency semantics, and statistically sound benchmarks.
Exercises
- Validate a port number and test invalid text, zero, and values above the limit.
- Profile a deliberately inefficient function and optimize only after identifying the bottleneck.
- Compare a thread pool and process pool for I/O-bound and CPU-bound tasks.
- Threat-model a program that reads a user-supplied JSON file.
Threat modeling and secure defaults
Identify assets, trust boundaries, attacker capabilities, and failure impact before selecting defenses. A file parser exposed to untrusted input needs size limits, structural validation, and bounded work. A web endpoint needs authentication and authorization as well as input validation. Security depends on the system around the Python code, so do not treat one validation function as a complete defense.
Use least privilege for processes and credentials. Avoid dynamic evaluation of user-controlled strings with eval or exec. Do not load untrusted pickle files. Use secure random tokens from secrets, parameterized database statements, and subprocess argument lists. Keep dependencies current and monitor advisories for the exact versions deployed.
Concurrency models and runtime variants
Threads share memory and are convenient for blocking I/O, but shared mutation needs synchronization. Asyncio multiplexes cooperative tasks on an event loop; blocking work must not stall that loop. Processes have separate memory spaces and can use multiple CPU cores, but require serialization or inter-process communication. Free-threaded interpreter builds change some concurrency limits but do not make shared mutable state automatically safe.
Benchmark the actual implementation and deployment. Extension modules may release or depend on the GIL, process startup varies by platform, and workload granularity determines whether coordination overhead is worthwhile. Choose a concurrency model from the task's blocking behavior, CPU needs, memory budget, and cancellation requirements.
Performance measurement and profiling
Use deterministic inputs and multiple trials. A microbenchmark should isolate the operation, but a system benchmark must include meaningful setup and I/O. Avoid comparing runs under different power, cache, interpreter, or dependency settings. Report medians and spread rather than only the fastest observation.
Optimization can trade memory for speed, simplicity for latency, and average throughput for predictable tail time. A cache needs an eviction policy; parallelism needs resource bounds; vectorization may change numeric precision. Retest correctness and resource use after every performance change.
Portability beyond syntax
Portable Python also depends on operating-system behavior, filesystem case sensitivity, newline conventions, process creation, terminal encoding, and native extensions. Test the supported matrix and isolate platform-specific operations. Pinning a package that contains a compiled extension may constrain operating systems, architectures, and Python ABI versions.
Mastery checkpoint
- Threat-model one input boundary and list abuse cases, mitigations, and remaining assumptions.
- Benchmark thread, async, and process approaches for a realistic I/O and CPU workload.
- Profile an application and justify an optimization with repeated measurements and a correctness test.
- Document supported Python implementations and platform limitations for a native dependency.