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.path requires you to thread a string through successive function calls: os.path.join(os.path.dirname(os.path.abspath(p)), "config.json'). The equivalent Path expression is Path(p).resolve().parent / 'config.json'. The / operator is overloaded on PurePosixPath and PureWindowsPath to 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 returns PosixPath('/var'). Both are surprising, but pathlib at least provides a single consistent type (PosixPath or WindowsPath) whose semantics are visible in the type.
Uniform access to filesystem operations. os.path only handles path manipulation. Actual I/O (reading, writing, stat, mkdir, glob) requires importing os``shutil``glob``statAnd others. Path objects carry methods for all of these: .read_text()``.write_text() .stat()``.mkdir()``.glob()``.rename()``.unlink().
Cross-platform correctness. os.path relies on the host operating system to determine separator behavior. pathlib exposes PurePosixPath and PureWindowsPath for explicit control when you need to manipulate paths for a different platform (e.g., generating URLs on a Linux server that target Windows).
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()) # True
print (config_file.stat().st_size) # byte count
print (config_file.suffix) # '.json'
print (config_file.stem) # 'settings'
pathlib does not cover everything in the os module. The following still require os directly:
os.environ: environment variables (pathlib has no equivalent).os.chdir()``os.getcwd(): changing and querying the current working directory.os.walk(): recursive directory traversal (though Path.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: Path does expand ~ in constructors but not $VAR).## These have no pathlib equivalent
env_home = os.environ.get( " HOME " , " /tmp " )
os.chmod( " /tmp/file.txt " , 0o 644 )
Even in new code, some os.path functions are unavoidable or more convenient than their pathlib Equivalents:
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_size
os.path.abspath(p) # Path(p).resolve()
os.path.basename(p) # Path(p).name
os.path.dirname(p) # Path(p).parent
os.path.join(a, b, c) # Path(a) / b / c
os.path.splitext(p) # Path(p).suffix, Path(p).stem
os.path.normpath(p) # Path(p) (constructor normalizes)
from pathlib import Path, PurePosixPath
p = PurePosixPath( " /usr/local/bin/python3.12 " )
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.suffixes) # ['.12']
PurePosixPath and PureWindowsPath perform only string manipulation — no filesystem access. This Is useful for constructing or parsing paths for remote systems.
The sys module exposes the runtime environment: interpreter configuration, the module search path, Reference counting, and process-level control.
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.
print ( f "Usage: { sys.argv[ 0 ] } <input> <output>" , file = sys.stderr)
input_file, output_file = sys.argv[ 1 ], sys.argv[ 2 ]
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). PYTHONPATH environment variable.Installation-dependent defaults (site-packages). ## ['/home/user/project', '/usr/lib/python312.zip', '/usr/lib/python3.12']
# Temporarily prepend a directory
sys.path.insert( 0 , " /opt/custom_libs " )
import mymodule # found in /opt/custom_libs first
Modifying 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 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.
print (sys.modules[ " json " ]) # <module 'json' from '...'>
sys.modules[ " json " ] = None # breaks all subsequent json imports
This 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() 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.
print ( f "Caught exit with code: { e.code } " ) # 42
# Process continues normally
This 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.
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).
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, date
def serialize_custom ( obj ):
if isinstance (obj, datetime):
if isinstance (obj, date):
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 ):
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 " ])
Property JSON Pickle Format Text Binary Language-agnostic Yes No (Python-only) Security Safe for untrusted data Never untrusted dataSupported 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.
def __init__ ( self , value , left = None , right = None ):
tree = Node( 1 , Node( 2 ), Node( 3 ))
data = pickle.dumps(tree)
restored = pickle.loads(data)
print (restored.value) # 1
print (restored.left.value) # 2
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.
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).
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 alternation
pattern = re.compile( r " (?: https ?| ftp ) :// ( [ \w ./] + ) ')
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 pipe
re.findall( r ' ^\w + ' , text, re. MULTILINE | re. IGNORECASE )
Unicode case folding is complex. If you are matching ASCII-only identifiers, use `re.ASCII` (or `re.A`) alongside `re.IGNORECASE` to constrain `\w``\b``\d`And `\s` to ASCII ranges. return word.upper() if word.islower() else word.lower()
result = re.sub( r ' \b\w + \b ' , replacer, " hello WORLD test " )
print (result) # HELLO world TEST
partial creates a new callable with some arguments pre-filled. This is not currying — it does not Transform a multi-argument function into a chain of single-argument functions. It binds Positional or keyword arguments.
from functools import partial
def power ( base , exponent ):
square = partial(power, exponent = 2 )
cube = partial(power, exponent = 3 )
partial is particularly useful for adapting function signatures to fit APIs that expect a specific Callable form, such as map``sortedOr callback interfaces.
from functools import partial
pairs = [( 1 , 2 ), ( 3 , 1 ), ( 2 , 4 )]
sorted (pairs, key = partial( lambda seq , idx : seq[idx], idx = 1 ))
# [(3, 1), (1, 2), (2, 4)]
lru_cache is a decorator that memoizes function calls using a Least Recently Used eviction policy. It stores the mapping from arguments to return values in a dictionary-ordered structure (the CPython Implementation uses a doubly-linked list combined with a hash table, giving O(1) lookup and O(1) Eviction).
from functools import lru_cache
return fibonacci(n - 1 ) + fibonacci(n - 2 )
print (fibonacci( 100 )) # 354224848179261915075, computed instantly
print (fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
The cache key is the function’s positional and keyword arguments. All arguments must be hashable. This means you cannot cache functions that accept unhashable arguments (lists, dicts, sets) without A wrapper that converts them to a hashable representation.
from functools import lru_cache
def cached_parse ( query_string ):
return frozenset (query_string.split( " & " ))
maxsize=None creates an unbounded cache. Use this only when the argument space is small and Finite. An unbounded cache on a function with unbounded input is a memory leak.
singledispatch creates a generic function that dispatches on the type of the first argument. It is Python’s answer to method overloading for standalone functions.
from functools import singledispatch
from collections.abc import Sequence
raise NotImplementedError ( f "Cannot process { type (value) } " )
return [process(item) for item in value]
print (process( " hello " )) # HELLO
print (process([ 1 , " a " ])) # [2, 'A']
The register attribute can also be used as a decorator with explicit type arguments: @process.register(list). When used with type annotations, the annotation is extracted and used as The dispatch key.
wraps is a decorator factory that copies metadata (__name__``__doc__``__module__ __annotations__``__dict__) from the wrapped function to the wrapper function. Without it, every Decorated function appears as wrapper in tracebacks, help()And sphinx documentation.
from functools import wraps
def retry ( max_attempts = 3 ):
def wrapper ( * args , ** kwargs ):
for attempt in range ( 1 , max_attempts + 1 ):
return func( * args, ** kwargs)
if attempt == max_attempts:
print (fetch_data. __name__ ) # 'fetch_data', not 'wrapper'
The itertools module provides fast, memory-efficient tools for working with iterators. Every Function in this module returns an iterator — no intermediate lists are created.
chain takes multiple iterables and produces a single iterator that yields from each in sequence. chain.from_iterable takes a single iterable of iterables.
from itertools import chain
list (chain([ 1 , 2 ], [ 3 , 4 ], [ 5 ])) # [1, 2, 3, 4, 5]
list (chain.from_iterable([[ 1 , 2 ], [ 3 , 4 ], [ 5 ]])) # [1, 2, 3, 4, 5]
These functions generate elements from the Cartesian product and combinatorial selections of input Iterables.
from itertools import product, permutations, combinations, combinations_with_replacement
print ( list (product([ 1 , 2 ], [ ' a ' , ' b ' ])))
# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
print ( list (permutations([ 1 , 2 , 3 ], 2 )))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
print ( list (combinations([ 1 , 2 , 3 ], 2 )))
# [(1, 2), (1, 3), (2, 3)]
print ( list (combinations_with_replacement([ 1 , 2 , 3 ], 2 )))
# [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)]
Key distinction: permutations considers order and does not repeat elements. combinations ignores Order and does not repeat. product considers order and repeats according to the repeat Parameter. combinations_with_replacement ignores order but allows repetition.
groupby groups consecutive elements that share the same key. The input must be sorted by the Key — groupby does not sort for you.
from itertools import groupby
data = [( " apple " , 1 ), ( " banana " , 1 ), ( " cherry " , 2 ), ( " date " , 2 ), ( " elderberry " , 3 )]
for key, group in groupby(data, key =lambda x : x[ 1 ]):
# 1 [('apple', 1), ('banana', 1)]
# 2 [('cherry', 2), ('date', 2)]
The groups are iterators that are consumed as you iterate over them. If you need to revisit a group, Convert it to a list first.
from itertools import islice, filterfalse, accumulate
# islice: slice an iterator without materializing it
fib = (a + b for a, b in zip ( range ( 10 ), range ( 1 , 11 )))
print ( list (islice(fib, 5 ))) # [1, 3, 5, 7, 9]
# filterfalse: opposite of filter
print ( list (filterfalse( lambda x : x % 2 == 0 , range ( 10 ))))
# accumulate: running totals (or any binary function)
print ( list (accumulate([ 1 , 2 , 3 , 4 , 5 ])))
print ( list (accumulate([ 1 , 2 , 3 , 4 , 5 ], operator.mul)))
Type annotations serve three audiences: static type checkers (mypy, pyright, pytype), IDEs (autocompletion, refactoring), and human readers (documentation). Python does not enforce type Annotations at runtime — they are ignored by the interpreter and stored only in the __annotations__ attribute.
from typing import Union, Optional, Literal, Callable, Any, TypeAlias
def process ( value : Union[ int , str ]) -> str :
# Equivalent with 3.10+ syntax
def process ( value : int | str ) -> str :
# Optional[X] is Union[X, None]
def find_user ( user_id : int ) -> Optional[ str ]:
# Literal restricts to specific values
def set_level ( level : Literal[ " debug " , " info " , " warning " , " error " ]) -> None :
# Callable describes function signatures
def apply ( func : Callable[[ int , int ], int ], a : int , b : int ) -> int :
# Any disables type checking for a value
data: Any = json.loads(raw_string)
Protocol (Python 3.8+) enables structural typing. A class is considered a subtype of a Protocol if It has the right methods, regardless of whether it explicitly inherits from the Protocol.
from typing import Protocol, runtime_checkable
class Drawable ( Protocol ):
def render_all ( items : list[Drawable]) -> list[ str ]:
return [item.draw() for item in items]
render_all([Circle()]) # OK
render_all([Square()]) # mypy error: "Square'' has no "draw' method
@runtime_checkable adds isinstance() support, but only checks for the presence of method names — not their signatures.
class Closeable ( Protocol ):
print ( isinstance (FileHandle(), Closeable)) # True
from typing import TypeAlias, ParamSpec, Concatenate, Callable
# TypeAlias: explicit type alias declaration
JSON : TypeAlias = dict[ str , Any]
UserID: TypeAlias = int | str
# ParamSpec: capture the parameter signature of a callable
def log_calls ( func : Callable[P, None ]) -> Callable[P, None ]:
def wrapper ( * args : P.args, ** kwargs : P.kwargs) -> None :
print ( f "Calling { func. __name__} " )
# Concatenate: prepend parameters to a captured signature
func : Callable[Concatenate[ int , P], None ]
) -> Callable[Concatenate[ int , P], None ]:
def wrapper ( user_id : int , * args : P.args, ** kwargs : P.kwargs) -> None :
func(user_id, * args, ** kwargs)
ParamSpec solves a real problem: without it, a decorator that preserves the signature of arbitrary Callables must use *args, **kwargs with untyped AnyLosing all type information about the Wrapped function’s parameters. ParamSpec captures the full parameter signature as a type variable.
The @dataclass decorator (covered in the OOP chapter for fundamentals) supports advanced patterns Through the field() function, __post_init__And inheritance.
from dataclasses import dataclass, field
_area: float = field( init = False , repr = False )
if self .width <= 0 or self .height <= 0 :
raise ValueError ( " Dimensions must be positive " )
self ._area = self .width * self .height
__post_init__ is called after the generated __init__. It receives the same arguments as __init__. Fields with init=False are not passed to __init__ and must be set in __post_init__.
from dataclasses import dataclass, field
def validate_positive ( value : float ) -> float :
raise ValueError ( " Must be positive " )
retries: int = field( default = 3 , validator =lambda v : v >= 0 )
tags: list[ str ] = field( default_factory = list )
created_at: float = field( default_factory = time.time, repr = False , compare = False )
field() parameterEffect defaultStatic default value (never use for mutable types) default_factoryZero-arg callable producing the default value initInclude in __init__ (default True) reprInclude in __repr__ (default True) compareInclude in __eq__/__hash__ (default True) hashInclude in __hash__ (default NoneInherits) metadataArbitrary dict for external tools
from dataclasses import dataclass
slots=True (Python 3.10+) generates __slots__ automatically. Every class in the hierarchy must Use slots=True — mixing slotted and non-slotted dataclasses in an inheritance chain raises TypeError.
Raw integer constants have no type identity. If two modules define STATUS_OK = 0 and ERROR_NONE = 0They are indistinguishable — they are both int with value 0. This causes Silent bugs in comparisons and makes debugging harder because log messages show bare integers.
IntEnum members are both integers and enum members. They compare equal to their integer values (for backward compatibility) but have a distinct type and repr.
from enum import IntEnum, Enum, auto
print (Status. OK == 0 ) # True (IntEnum compares as int)
print (Status. OK == Status. OK ) # True
print ( repr (Status. OK )) # <Status.OK: 0>
print ( isinstance (Status. OK , Status)) # True
print ( isinstance ( 0 , Status)) # False
from enum import Enum, IntEnum, StrEnum, auto
print (Color. RED == 1 ) # False (plain Enum does not compare as int)
print (Priority. HIGH == 3 ) # True
print (Role. ADMIN == " admin " ) # True
Use Enum when members have no natural comparison with primitive types and you want strict type safety. Use IntEnum when members must interoperate with C APIs or integer-based protocols. Use StrEnum (Python 3.11+) when members represent string constants that are also used in serialization or string comparisons. Single process, using `is` for comparison is a fragile pattern that does not work correctly across Pickling, multiprocess serialization, or when the enum is re-imported.print writes to stdout unconditionally. It has no severity levels, no filtering, no routing to Files or network endpoints, no timestamps, no module attribution, and no ability to be disabled Without modifying source code. In a library, print is not just unprofessional — it is actively Harmful because it pollutes the consumer’s stdout with messages the consumer did not request and Cannot control.
The logging module solves all of these problems:
Severity levels (DEBUG``INFO``WARNING``ERROR``CRITICAL) allow you to control verbosity without changing code.Loggers are hierarchical. A logger named "myapp.db" inherits configuration from "myapp". Libraries use loggers named after their module (__name__), and the application configures them centrally.Handlers route messages to different destinations: stdout, stderr, files, sockets, email.Formatters control output format , including timestamps, logger names, severity, and message text.logger = logging.getLogger( __name__ )
def process_item ( item_id : int ) -> None :
logger.debug( " Processing item %d " , item_id)
logger.info( " Item %d processed successfully " , item_id)
logger.warning( " Item %d has deprecated field " , item_id)
Formatting until it determines that the message will actually be emitted. With f-strings, the string Is always constructed even if the log level is filtered out. `logger.debug("Expensive: %r", compute_value())` does not call `compute_value()` if DEBUG is not Enabled. `logger.debug(f"Expensive: {compute_value()}")` always calls it. format = " %(asctime)s [ %(levelname)s ] %(name)s : %(message)s " ,
datefmt = " %Y-%m- %d %H:%M:%S " ,
logger = logging.getLogger( " myapp " )
logger.info( " Application started " )
# 2025-06-04 14:00:00 [INFO] myapp: Application started
basicConfig is a convenience function that configures the root logger. It can only be called once — subsequent calls have no effect unless force=True is passed. For more complex configuration, Use dictConfig or fileConfig.
" disable_existing_loggers " : False ,
" format " : " %(asctime)s %(name)s %(levelname)s %(message)s " ,
" datefmt " : " %Y-%m- %d %H:%M:%S " ,
" format " : " %(levelname)s : %(message)s " ,
" class " : " logging.StreamHandler " ,
" stream " : " ext://sys.stderr " ,
" class " : " logging.handlers.RotatingFileHandler " ,
" handlers " : [ " console " , " file " ],
logging.config.dictConfig( LOGGING_CONFIG )
A handler determines where log messages go. Multiple handlers can be attached to the same logger.
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
logger = logging.getLogger( " myapp.service " )
file_handler = RotatingFileHandler(
maxBytes = 5 * 1024 * 1024 ,
file_handler.setFormatter(
logging.Formatter( " %(asctime)s %(levelname)s %(message)s " )
logger.addHandler(file_handler)
Common handlers:
StreamHandler: writes to a file-like object (stdout, stderr).FileHandler: writes to a single file.RotatingFileHandler: rotates when the file reaches a size limit.TimedRotatingFileHandler: rotates at time intervals (midnight, hourly, etc.).SysLogHandler: sends to Unix syslog.NullHandler: discards all messages. This is the recommended handler for library loggers.Libraries should never configure logging. They should create loggers with logging.getLogger(__name__) and attach a NullHandler so that logging does not produce warnings When no handler is configured.
logger = logging.getLogger( __name__ )
logger.addHandler(logging.NullHandler())
The datetime module provides four core types for temporal data. They are deliberately separate Because not every temporal concept needs all components.
from datetime import date, time, datetime, timedelta, timezone
dt = datetime( 2025 , 6 , 4 , 14 , 30 , 0 , tzinfo = timezone.utc)
delta = timedelta( days = 7 , hours = 3 )
print (d + delta) # 2025-06-11
print (dt + delta) # 2025-06-11 17:30:00+00:00
print (dt - datetime( 2025 , 1 , 1 )) # 154 days, 14:30:00
print (dt.isoformat()) # 2025-06-04T14:30:00+00:00
date stores year, month, day. time stores hour, minute, second, microsecond, and timezone info. datetime is the combination of both. timedelta represents a duration.
A naive datetime has no timezone information. It represents an abstract time that cannot be mapped To a specific instant on the timeline. An aware datetime carries a tzinfo subclass that defines Its offset from UTC.
from datetime import datetime, timezone, timedelta
naive = datetime( 2025 , 6 , 4 , 14 , 0 )
print (naive.tzinfo) # None
aware = datetime( 2025 , 6 , 4 , 14 , 0 , tzinfo = timezone.utc)
print (aware.tzinfo) # UTC
# Convert to a different timezone
eastern = timezone(timedelta( hours =- 4 ))
print (aware.astimezone(eastern)) # 2025-06-04 10:00:00-04:00
Timestamps that will be stored, transmitted, or compared across systems. Naive datetimes are Acceptable only for purely local display or when the timezone context is obvious and unambiguous (e.g., "schedule this for 9 AM in the user's local time").from datetime import datetime
# Parsing with strptime (strict format matching)
dt = datetime.strptime( " 2025-06-04 14:30 " , " %Y-%m- %d %H:%M " )
print (dt) # 2025-06-04 14:30:00
# Formatting with strftime
formatted = dt.strftime( " %B %d , %Y at %I:%M %p " )
print (formatted) # June 04, 2025 at 02:30 PM
# ISO format (for machine interchange)
print (dt.isoformat()) # 2025-06-04T14:30:00
print (datetime.fromisoformat( " 2025-06-04T14:30:00 " )) # round-trip
The most commonly used format codes:
Code Meaning Example %YFour-digit year 2025 %mZero-padded month 06 %dZero-padded day 04 %HHour (24-hour, zero-padded) 14 %IHour (12-hour, zero-padded) 02 %MMinute (zero-padded) 30 %SSecond (zero-padded) 00 %pAM/PM PM %fMicrosecond (zero-padded) 000000 %zUTC offset +0000 %ZTimezone name UTC %AFull weekday name Wednesday %BFull month name June
datetime.fromtimestamp() returns a local time by default. For a UTC datetime, use datetime.fromtimestamp(ts, tz=timezone.utc) or datetime.utcfromtimestamp() (deprecated since 3.12).
from datetime import datetime, timezone
local_dt = datetime.fromtimestamp(ts)
utc_dt = datetime.fromtimestamp(ts, tz = timezone.utc)
print (local_dt.tzinfo) # None (naive, but represents local time)
print (utc_dt.tzinfo) # UTC (aware)
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
yesterday = now - timedelta( days = 1 )
start_of_day = now.replace( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
# Iterating over date ranges
current += timedelta( days = 1 )
For more complex date arithmetic (business days, holidays, recurrence rules), the third-party dateutil library extends datetime with relativedelta``rruleAnd flexible parsing.
Confusing an algorithm with a program. An algorithm is a step-by-step procedure, not its implementation in code.
Misunderstanding the difference between a stack (LIFO) and a queue (FIFO) in data structure applications.
Mixing up Big O, Big Ω \Omega Ω , and Big Θ \Theta Θ notation. Big O is an upper bound, not necessarily tight.
Forgetting that O ( n log n ) O(n \log n) O ( n log n ) average-case for quicksort becomes O ( n 2 ) O(n^2) O ( n 2 ) worst-case on already sorted input.
The key principles covered in this topic are linked in the sub-pages above. Focus on understanding the definitions, applying the formulas or frameworks, and evaluating strengths and limitations of each approach.
Python’s standard library is a Swiss Army knife that comes pre-installed. The os and sys modules are your operating system遥控器, letting you interact with files, processes, and environment variables. pathlib is a modern replacement that treats file paths as objects with methods rather than strings to concatenate, the way you would use a GPS address instead of reading a paper map. collections provides specialized containers that are faster or more appropriate than plain lists and dicts for specific tasks, like using a screwdriver instead of a hammer when the job calls for precision.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
## Cross-References
Types and Variables — Standard library modules work with Python’s built-in types including lists, dicts, and strings.File I/O — The os and pathlib modules provide file system interaction beyond basic file reading and writing.Context Managers — Many standard library resources support the context manager protocol for safe resource management.