Essential Modules | Python - Wyatt's Notes
os and pathlib: File System Operations
Section titled “os and pathlib: File System Operations”The Case for pathlib Over os.path
Section titled “The Case for pathlib Over os.path”The os.path module is a collection of free functions that operate on path strings. It was designed In an era before Python had a coherent object model for paths. pathlib (Python 3.4+) replaces this With an object-oriented API where a Path instance represents a single filesystem path.
The argument for pathlib is not aesthetic preference. It is about composability and correctness:
Method chaining.
os.pathrequires you to thread a string through successive function calls:os.path.join(os.path.dirname(os.path.abspath(p)), "config.json'). The equivalentPathexpression isPath(p).resolve().parent / 'config.json'. The/operator is overloaded onPurePosixPathandPureWindowsPathto join path components, which reads as natural composition rather than nested function calls.No silent truncation.
os.path.join('/etc', '/var')returns/var— an absolute second argument silently discards the first.Path('/etc') / '/var'raises no error but returnsPosixPath('/var'). Both are surprising, butpathlibat least provides a single consistent type (PosixPathorWindowsPath) whose semantics are visible in the type.Uniform access to filesystem operations.
os.pathonly handles path manipulation. Actual I/O (reading, writing, stat, mkdir, glob) requires importingos``shutil``glob``statAnd others.Pathobjects carry methods for all of these:.read_text()``.write_text().stat()``.mkdir()``.glob()``.rename()``.unlink().Cross-platform correctness.
os.pathrelies on the host operating system to determine separator behavior.pathlibexposesPurePosixPathandPureWindowsPathfor explicit control when you need to manipulate paths for a different platform (e.g., generating URLs on a Linux server that target Windows).
from pathlib import Path
config_dir = Path.home() / ".config" / "myapp"config_dir.mkdir(parents=True, exist_ok=True)
config_file = config_dir / "settings.json"config_file.write_text('{"theme": "dark"}')
content = config_file.read_text()print(config_file.exists()) # Trueprint(config_file.stat().st_size) # byte countprint(config_file.suffix) # '.json'print(config_file.stem) # 'settings'When os Is Still Needed
Section titled “When os Is Still Needed”pathlib does not cover everything in the os module. The following still require os directly:
os.environ: environment variables (pathlibhas no equivalent).os.chdir()``os.getcwd(): changing and querying the current working directory.os.walk(): recursive directory traversal (thoughPath.rglob()covers most use cases).os.umask()``os.getuid()``os.setsid(): low-level process and permission operations.os.path.expandvars()``os.path.expanduser(): shell variable expansion (note:Pathdoes expand~in constructors but not$VAR).
import os
## These have no pathlib equivalentpid = os.getpid()env_home = os.environ.get("HOME", "/tmp")os.chmod("/tmp/file.txt", 0o644)os.path Functions Still Worth Knowing
Section titled “os.path Functions Still Worth Knowing”Even in new code, some os.path functions are unavoidable or more convenient than their pathlib Equivalents:
import os.path
os.path.exists(p) # Path(p).exists()os.path.isfile(p) # Path(p).is_file()os.path.isdir(p) # Path(p).is_dir()os.path.getsize(p) # Path(p).stat().st_sizeos.path.abspath(p) # Path(p).resolve()os.path.basename(p) # Path(p).nameos.path.dirname(p) # Path(p).parentos.path.join(a, b, c) # Path(a) / b / cos.path.splitext(p) # Path(p).suffix, Path(p).stemos.path.normpath(p) # Path(p) (constructor normalizes)Path Parts and Components
Section titled “Path Parts and Components”from pathlib import Path, PurePosixPath
p = PurePosixPath("/usr/local/bin/python3.12")
print(p.anchor) # '/'print(p.drive) # ''print(p.parts) # ('/', 'usr', 'local', 'bin', 'python3.12')print(p.parent) # PurePosixPath('/usr/local/bin')print(p.name) # 'python3.12'print(p.stem) # 'python3.12'print(p.suffix) # '.12'print(p.suffixes) # ['.12']PurePosixPath and PureWindowsPath perform only string manipulation — no filesystem access. This Is useful for constructing or parsing paths for remote systems.
sys: Interpreter State
Section titled “sys: Interpreter State”The sys module exposes the runtime environment: interpreter configuration, the module search path, Reference counting, and process-level control.
sys.argv: Command-Line Arguments
Section titled “sys.argv: Command-Line Arguments”sys.argv is a list of strings. sys.argv[0] is the script name (or '-' for stdin). Everything After is a positional argument. It does not handle options, flags, or defaults — for that, use argparse.
import sys
if len(sys.argv) != 3: print(f"Usage: {sys.argv[0]} <input> <output>", file=sys.stderr) sys.exit(1)
input_file, output_file = sys.argv[1], sys.argv[2]sys.path: Module Search Path
Section titled “sys.path: Module Search Path”When you write import fooPython searches for foo in the directories listed in sys.path. The First match wins. The initial value is populated from:
- The directory containing the script (or the current directory for interactive mode).
PYTHONPATHenvironment variable.- Installation-dependent defaults (site-packages).
import sys
print(sys.path[:3])## ['/home/user/project', '/usr/lib/python312.zip', '/usr/lib/python3.12']
# Temporarily prepend a directorysys.path.insert(0, "/opt/custom_libs")import mymodule # found in /opt/custom_libs firstModifying sys.path at runtime is fragile. For reproducible imports, use proper package Installation or PYTHONPATH. Mutating sys.path in library code is particularly dangerous because It affects the global import state of the entire process.
sys.modules: The Module Cache
Section titled “sys.modules: The Module Cache”sys.modules is a dictionary mapping module names to loaded module objects. The import system Checks this dictionary first — if a module is already loaded, import returns the cached object Without re-executing the module’s code.
import sysimport json
print(sys.modules["json"]) # <module 'json' from '...'>sys.modules["json"] = None # breaks all subsequent json importsThis is occasionally useful for reloading modules during development or for testing, but modifying sys.modules in production code is almost always a mistake.
sys.exit()
Section titled “sys.exit()”sys.exit() raises SystemExitWhich the interpreter catches at the top level to terminate the Process with the given exit code. Because it is an exception, it can be caught and handled — finally blocks and context managers still execute.
import sys
try: sys.exit(42)except SystemExit as e: print(f"Caught exit with code: {e.code}") # 42# Process continues normallyThis is why sys.exit() is preferred over os._exit(). os._exit() terminates the process Immediately without cleanup: no finally blocks, no atexit handlers, no buffer flushing.
json: Serialization
Section titled “json: Serialization”Encoding and Decoding
Section titled “Encoding and Decoding”import json
data = {"users": [{"name": "Alice", "active": True}, {"name": "Bob", "active": False}]}
serialized = json.dumps(data, indent=2, sort_keys=True)deserialized = json.loads(serialized)
print(type(serialized)) # <class 'str'>print(type(deserialized)) # <class 'dict'>json.dumps() returns a string. json.dump() writes directly to a file object. The symmetric pair Is json.loads() (from string) and json.load() (from file object).
Custom Encoders and Decoders
Section titled “Custom Encoders and Decoders”The default parameter of json.dumps() is a function called for objects that are not natively Serializable (i.e., not dict``list``str``int``float``boolOr None).
from datetime import datetime, dateimport json
def serialize_custom(obj): if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, date): return obj.isoformat() if isinstance(obj, set): return sorted(obj) raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
data = {"created": datetime(2025, 6, 4, 14, 0), "tags": {"python", "stdlib"}}print(json.dumps(data, default=serialize_custom, indent=2))For more control, subclass json.JSONEncoder and override default():
class CustomEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return {"__type__": "datetime", "value": obj.isoformat()} return super().default(obj)
class CustomDecoder(json.JSONDecoder): def __init__(self, *args, **kwargs): super().__init__(object_hook=self._object_hook, *args, **kwargs)
def _object_hook(self, dct): if dct.get("__type__") == "datetime": return datetime.fromisoformat(dct["value"]) return dctJSON vs Pickle
Section titled “JSON vs Pickle”| Property | JSON | Pickle |
|---|---|---|
| Format | Text | Binary |
| Language-agnostic | Yes | No (Python-only) |
| Security | Safe for untrusted data | Never untrusted data |
| Supported types | Primitives, dict, list, str | Almost any Python object |
| Human-readable | Yes | No |
| Version-stable | Yes (RFC 8259) | No (protocol changes between versions) |
Pickle can serialize functions, classes, and object graphs with cycles. But pickle.loads() on Untrusted data is equivalent to arbitrary code execution — the pickled byte stream can contain Instructions to call any callable, import any module, and execute arbitrary code. For data Interchange between systems or for storage that must survive Python version upgrades, JSON is the Only safe choice.
import pickle
class Node: def __init__(self, value, left=None, right=None): self.value = value self.left = left self.right = right
tree = Node(1, Node(2), Node(3))data = pickle.dumps(tree)restored = pickle.loads(data)print(restored.value) # 1print(restored.left.value) # 2re: Regular Expressions
Section titled “re: Regular Expressions”Pattern Fundamentals
Section titled “Pattern Fundamentals”Python’s re module uses a backtracking NFA engine. Patterns are compiled into bytecode that the Engine interprets. Compilation is the expensive step; matching is fast on the compiled pattern.
import re
pattern = re.compile(r'\b(\w+)@(\w+)\.(\w+)\b')match = pattern.search("Contact alice@example.com or bob@test.org")print(match.group(0)) # 'alice@example.com'print(match.group(1)) # 'alice'print(match.group(2)) # 'example'print(match.group(3)) # 'com'print(match.groups()) # ('alice', 'example', 'com')Always use raw strings (r'...') for regex patterns. Without the raw prefix, \b is interpreted as A backspace character, and \d``\w``\s are interpreted as escape sequences (some of which are Valid in Python strings, producing the wrong character in the regex).
Named Groups and Non-Capturing Groups
Section titled “Named Groups and Non-Capturing Groups”import re
pattern = re.compile(r'(?P<user>\w+)@(?P<domain>[\w.]+)')match = pattern.match("alice@example.com")print(match.group("user")) # 'alice'print(match.group("domain")) # 'example.com'print(match.groupdict()) # {'user': "alice'', "domain': "example.com''}Non-capturing groups (?:...) participate in alternation and quantification but do not create a Backreference. This prevents group numbering from shifting when you add groups for structural Purposes.
# Non-capturing group for alternationpattern = re.compile(r"(?:https?|ftp)://([\w./]+)')import re
text = "Hello\nWorld"
re.findall(r'^\w+', text) # ['Hello'] (default: ^ matches start of string)re.findall(r'^\w+', text, re.MULTILINE) # ['Hello', 'World']
re.findall(r'hello', "Hello World") # []re.findall(r'hello', "Hello World", re.IGNORECASE) # ['Hello']
# Combining flags with pipere.findall(r'^\w+', text, re.MULTILINE | re.IGNORECASE)Intuition
Section titled “Intuition”Python’s standard library is “batteries included”: it ships with modules for almost everything. os and sys interact with the operating system, pathlib handles file paths in a cross-platform way, json and csv parse common data formats, and logging provides structured output. The key insight is to check the standard library before reaching for a third-party package. Most common tasks, from email parsing to HTTP requests to regular expressions, are already built in.