Python Common Modules
Python’s standard library includes a wealth of ready-to-use modules. This article introduces the ones most commonly needed in day-to-day development: serialization, path manipulation, time handling, system interfaces, logging, and more.
json — Cross-Language Serialization
JSON is a universal data exchange format supported by virtually every programming language. Python’s built-in json module converts between JSON and Python data structures:
import json
# Python → JSON string
data = {"name": "Alice", "age": 25, "scores": [90, 85, 92]}
text = json.dumps(data, ensure_ascii=False, indent=2)
print(text)
# {
# "name": "Alice",
# "age": 25,
# "scores": [90, 85, 92]
# }
# JSON string → Python
parsed = json.loads(text)
print(parsed["name"]) # Alice
# write to file
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# read from file
with open("data.json", encoding="utf-8") as f:
loaded = json.load(f)json supports these types: str, int, float, bool, list, dict, None. Custom objects require subclassing JSONEncoder:
import json
from datetime import datetime
class DateEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
data = {"created_at": datetime(2024, 6, 1, 12, 0)}
print(json.dumps(data, cls=DateEncoder))
# {"created_at": "2024-06-01T12:00:00"}pickle — Python-Specific Serialization
import pickle
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
user = User("Bob", 30)
# serialize to bytes
data = pickle.dumps(user)
# deserialize from bytes
user2 = pickle.loads(data)
print(user2.name, user2.age) # Bob 30
# write to / read from file (must use binary mode)
with open("user.pkl", "wb") as f:
pickle.dump(user, f)
with open("user.pkl", "rb") as f:
user3 = pickle.load(f)pathlib — Modern Path Operations (Python 3.4+, Recommended)
pathlib.Path handles file paths in an object-oriented way and is the modern replacement for os.path:
from pathlib import Path
# create a path object (automatically adapts to the OS separator)
p = Path("/home/user/docs/readme.txt")
p = Path(".") # current directory
# path joining (use the / operator)
config = Path("/etc") / "app" / "config.yaml"
# path information
p = Path("/home/user/docs/readme.txt")
print(p.name) # readme.txt
print(p.stem) # readme
print(p.suffix) # .txt
print(p.parent) # /home/user/docs
print(p.parts) # ('/', 'home', 'user', 'docs', 'readme.txt')
# checks
print(p.exists()) # whether it exists
print(p.is_file()) # whether it is a file
print(p.is_dir()) # whether it is a directory
# read/write text (automatically handles open/close)
path = Path("hello.txt")
path.write_text("Hello, World!", encoding="utf-8")
content = path.read_text(encoding="utf-8")
# read/write bytes
path.write_bytes(b"\x00\x01\x02")
data = path.read_bytes()
# iterate over a directory
for f in Path(".").iterdir():
print(f)
# glob pattern matching
for py_file in Path("src").glob("**/*.py"): # recursively find all .py files
print(py_file)
# create / delete
Path("new_dir").mkdir(parents=True, exist_ok=True)
Path("file.txt").unlink(missing_ok=True) # delete a file
# get file size and modification time
stat = path.stat()
print(stat.st_size) # size in bytesos — Operating System Interface
import os
# environment variables
print(os.environ.get("PATH"))
os.environ["MY_VAR"] = "hello"
# current working directory
print(os.getcwd())
os.chdir("/tmp")
# run a system command
os.system("ls -la") # does not capture output
# run a command and capture its output (subprocess is recommended)
import subprocess
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
print(result.stdout)
# file/directory operations
os.makedirs("a/b/c", exist_ok=True) # create directories recursively
os.rename("old.txt", "new.txt")
os.remove("file.txt")
os.rmdir("empty_dir")
# list a directory
print(os.listdir("."))
# system info
print(os.name) # 'posix' (Linux/Mac) or 'nt' (Windows)
print(os.sep) # '/' or '\\'
print(os.getpid()) # current process IDpathlib.Path over os.path. os.path still works but produces more verbose code.shutil — High-Level File Operations
import shutil
# copy a file (content only)
shutil.copyfile("src.txt", "dst.txt")
# copy a file (content + permissions)
shutil.copy("src.txt", "dst/")
# copy a file (content + permissions + metadata)
shutil.copy2("src.txt", "dst/")
# recursively copy a directory
shutil.copytree("src_dir", "dst_dir") # dst_dir must not already exist
# move a file or directory
shutil.move("old_path", "new_path")
# recursively delete a directory (dangerous — double-check the path!)
shutil.rmtree("dir_to_delete")
# archive as zip/tar
shutil.make_archive("output", "zip", "source_dir")
shutil.make_archive("output", "gztar", "source_dir")
# extract
shutil.unpack_archive("output.zip", "extract_to/")sys — Python Interpreter Interface
import sys
# command-line arguments (sys.argv[0] is the script name)
# running: python script.py arg1 arg2
print(sys.argv) # ['script.py', 'arg1', 'arg2']
# Python version
print(sys.version) # '3.12.0 (main, ...) [GCC ...]'
print(sys.version_info) # sys.version_info(major=3, minor=12, ...)
# platform
print(sys.platform) # 'linux' / 'darwin' / 'win32'
# module search path
print(sys.path)
sys.path.insert(0, "/my/custom/modules") # insert a custom path at the front
# standard I/O
sys.stdout.write("Hello\n")
sys.stderr.write("Error message\n")
# exit the program (0 = normal, non-zero = error)
# sys.exit(0)
# recursion depth limit
print(sys.getrecursionlimit()) # 1000
sys.setrecursionlimit(2000)
# object memory size
import sys
lst = [1, 2, 3]
print(sys.getsizeof(lst)) # bytesargparse — Command-Line Argument Parsing
When a script needs to accept command-line arguments, the standard library’s argparse is a more professional choice than manually parsing sys.argv — it auto-generates help text and validates argument types for you:
import argparse
parser = argparse.ArgumentParser(
prog="mytool",
description="An example command-line tool",
)
# Positional argument: required by default; nargs='?' + default makes it optional
parser.add_argument("path", nargs="?", default=".", help="Directory to process, defaults to the current directory")
# Optional argument: boolean flag
parser.add_argument("-a", "--all", action="store_true", help="Show hidden files starting with .")
# Optional argument: with type conversion and a default value
parser.add_argument("-n", "--limit", type=int, default=10, help="Maximum number of entries to show")
args = parser.parse_args()
print(args)
# Namespace(path='.', all=False, limit=10)
print(args.path, args.all, args.limit)$ python mytool.py -h
usage: mytool [-h] [-a] [-n LIMIT] [path]
An example command-line tool
positional arguments:
path Directory to process, defaults to the current directory
options:
-h, --help show this help message and exit
-a, --all Show hidden files starting with .
-n LIMIT, --limit LIMIT
Maximum number of entries to show
$ python mytool.py /var/log -a -n 20
Namespace(path='/var/log', all=True, limit=20)Commonly used parameters:
| Parameter | Purpose |
|---|---|
nargs | Number of values: ? means 0 or 1, * means any number, + means at least 1 |
default | Value used when the argument isn’t provided |
type | Automatic type conversion, e.g. type=int, type=float |
choices | Restrict the argument to one of a fixed set of values |
action="store_true" | Boolean flag — True if present |
dest | Custom attribute name for the parsed value on the Namespace |
git commit, git push — a “tool + subcommand” shape), parser.add_subparsers() lets you define separate arguments per subcommand. It’s the standard way to build more complex CLI tools.datetime — Dates and Times
datetime is Python’s modern module for handling dates and times, and is easier to use than the time module:
from datetime import datetime, date, timedelta, timezone
# get the current time
now = datetime.now() # local time (no timezone)
utcnow = datetime.now(timezone.utc) # UTC time (recommended)
print(now) # 2024-06-01 14:30:00.123456
print(now.year) # 2024
print(now.strftime("%Y-%m-%d %H:%M:%S")) # 2024-06-01 14:30:00
# parse a time string
dt = datetime.strptime("2024-06-01 14:30:00", "%Y-%m-%d %H:%M:%S")
# time arithmetic
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)
diff = datetime(2024, 12, 31) - now
print(f"{diff.days} days until the end of the year")
# date only
today = date.today()
print(today.isoformat()) # 2024-06-01
# timestamp conversion
import time
ts = now.timestamp() # datetime → timestamp (float)
dt2 = datetime.fromtimestamp(ts) # timestamp → datetimemath — Mathematical Operations
import math
print(math.pi) # 3.141592653589793
print(math.e) # 2.718281828459045
print(math.tau) # 6.283... (2π)
print(math.ceil(4.1)) # 5 (round up)
print(math.floor(4.9)) # 4 (round down)
print(math.trunc(4.9)) # 4 (truncate decimal part)
print(math.sqrt(16)) # 4.0
print(math.pow(2, 10)) # 1024.0
print(math.log(100, 10)) # 2.0 (base-10 logarithm)
print(math.log2(1024)) # 10.0
print(math.log10(1000)) # 3.0
print(math.fabs(-3.14)) # 3.14 (floating-point absolute value)
print(math.gcd(48, 18)) # 6 (greatest common divisor; Python 3.9+ supports multiple args)
print(math.lcm(4, 6)) # 12 (least common multiple, Python 3.9+)
print(math.factorial(10)) # 3628800
# trigonometric functions (arguments in radians)
print(math.sin(math.pi / 2)) # 1.0
print(math.degrees(math.pi)) # 180.0
print(math.radians(180)) # 3.14...random — Random Numbers
import random
# basic random numbers
print(random.random()) # [0.0, 1.0)
print(random.uniform(1.5, 3.5)) # float in the given range
print(random.randint(1, 6)) # closed-interval integer [1, 6] (simulates a die roll)
print(random.randrange(0, 10, 2)) # one of 0, 2, 4, 6, 8
# sequence operations
items = ["apple", "banana", "cherry", "date"]
print(random.choice(items)) # pick one at random
print(random.choices(items, k=3)) # pick 3 with replacement (duplicates allowed)
print(random.sample(items, k=3)) # pick 3 without replacement (no duplicates)
shuffled = items.copy()
random.shuffle(shuffled) # in-place shuffle
print(shuffled)
# set a random seed (for reproducible results)
random.seed(42)
print([random.randint(1, 100) for _ in range(5)])
# always the same result: depends on the implementationlogging — Logging
logging is more suitable for production code than print, providing log levels, formatting, and output destination control:
import logging
# five levels (lowest to highest): DEBUG < INFO < WARNING < ERROR < CRITICAL
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__) # named after the module
logger.debug("Debug info, only visible during development")
logger.info("Normal program execution record")
logger.warning("Warning: config item missing, using default value")
logger.error("Error: database connection failed")
logger.critical("Critical: system is about to crash")Output to Both File and Console
import logging
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# file handler
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.WARNING) # only WARNING and above goes to the file
# console handler
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.DEBUG) # all levels go to the console
# shared format
fmt = logging.Formatter("%(asctime)s [%(levelname)-8s] %(message)s")
file_handler.setFormatter(fmt)
stream_handler.setFormatter(fmt)
logger.addHandler(file_handler)
logger.addHandler(stream_handler)
logger.info("Server started")
logger.error("Connection timed out")Logging Configuration (Recommended Approach)
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "DEBUG",
},
"file": {
"class": "logging.FileHandler",
"filename": "app.log",
"formatter": "standard",
"level": "WARNING",
"encoding": "utf-8",
},
},
"root": {"handlers": ["console", "file"], "level": "DEBUG"},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)zipfile — ZIP Archives
import zipfile
# create a zip archive
with zipfile.ZipFile("archive.zip", "w", zipfile.ZIP_DEFLATED) as zf:
zf.write("file1.txt")
zf.write("file2.txt", arcname="renamed.txt") # rename when storing
# extract
with zipfile.ZipFile("archive.zip", "r") as zf:
zf.extractall("output_dir/") # extract all
zf.extract("renamed.txt", "output_dir/") # extract a specific file
print(zf.namelist()) # list contents
# append a file
with zipfile.ZipFile("archive.zip", "a") as zf:
zf.write("new_file.txt")tarfile — TAR Archives
import tarfile
# create a .tar.gz
with tarfile.open("archive.tar.gz", "w:gz") as tf:
tf.add("src_dir/", arcname="backup") # archive an entire directory
# extract
with tarfile.open("archive.tar.gz", "r:gz") as tf:
tf.extractall("output_dir/")
print(tf.getnames()) # list contents
# format reference:
# "w" → .tar (pack only, no compression)
# "w:gz" → .tar.gz (gzip compression)
# "w:bz2" → .tar.bz2 (bzip2 compression, usually smaller)
# "w:xz" → .tar.xz (xz compression, highest compression ratio)