Primeros Pasos
Hola Mundo y Comentarios
Python usa # para comentarios y comillas triples para docstrings. La función print() admite los parámetros sep y end para personalizar el formato de salida. Los docstrings sirven como documentación accesible vía help() y __doc__.
# This is a single-line comment
"""
This is a
multi-line comment (docstring)
"""
print("Hello, World!") # print to stdout
print("A", "B", "C", sep="-") # A-B-C
print("No newline", end="") # suppress newlineIndentación y Bloques de Código
A diferencia de la mayoría de lenguajes, Python usa indentación en lugar de llaves para definir bloques de código. La consistencia es crítica: mezclar tabuladores y espacios provoca SyntaxError. PEP 8 recomienda 4 espacios por nivel.
# Python uses indentation (4 spaces) to define blocks
if True:
print("inside if")
if True:
print("nested block")
print("outside block")
# No braces! Indentation IS the syntax
def func():
x = 1
return x + 1Entrada y Salida
input() lee de stdin como una cadena: convierte siempre cuando necesites un número. Usa int(), float(), etc. para la conversión. Los f-strings (Python 3.6+) son la forma preferida de formatear cadenas.
# input() always returns a string
name = input("Enter your name: ")
age = int(input("Enter your age: ")) # convert to int
print(f"Hello {name}, you are {age} years old")
# formatted output
print("Pi is approximately {:.2f}".format(3.14159))
print(f"{1000000:,}") # 1,000,000 with thousands separatorMúltiples Sentencias y Continuación de Línea
Usa punto y coma para separar sentencias en una línea (poco común en Python idiomático). Las líneas largas pueden continuarse con barra invertida, o automáticamente dentro de (), [], {}. Prefiere la continuación implícita por legibilidad.
# multiple statements on one line (discouraged)
a = 1; b = 2; c = 3
# explicit line continuation
total = 1 + 2 + 3 + \
4 + 5 + 6
# implicit continuation inside brackets
nums = [
1, 2, 3,
4, 5, 6
]
result = (1 + 2
+ 3 + 4)Ejecución de Python
Los scripts de Python se ejecutan con 'python script.py'. El REPL permite experimentación interactiva. Usa siempre python3 en sistemas donde python apunta a Python 2. La línea shebang hace ejecutables a los scripts en Unix.
# Run a script
# $ python script.py
# Run interactively (REPL)
# $ python
# >>> 2 + 2
# 4
# Shebang line for Unix scripts
#!/usr/bin/env python3
# Check Python version
import sys
print(sys.version)
print(sys.version_info.major) # 3Variables y Tipos de Datos
Variables y Tipado Dinámico
Python usa tipado dinámico: las variables pueden cambiar de tipo en tiempo de ejecución. Usa type() para comprobar, isinstance() para verificar. Python 3.6+ admite anotaciones de tipo (name: str = 'Alice') para soporte en el IDE sin imposición en tiempo de ejecución.
# Python is dynamically typed - no declaration needed
name = "Alice" # str
age = 30 # int
height = 5.7 # float
is_active = True # bool
items = [1, 2, 3] # list
# Type checking
print(type(name)) # <class 'str'>
print(isinstance(age, int)) # True
# Multiple assignment
x, y, z = 1, 2, 3
a = b = 0 # chain assignmentAnotaciones de Tipo (Python 3.6+)
Las anotaciones de tipo mejoran la legibilidad del código y habilitan el autocompletado del IDE y el análisis estático con mypy. NO se imponen en tiempo de ejecución: Python sigue siendo de tipado dinámico. Usa Optional[X] para valores que pueden ser None.
# Variable annotations
name: str = "Alice"
age: int = 30
scores: list[float] = [90.5, 85.0]
# Function annotations
def greet(name: str, times: int = 1) -> str:
return (f"Hi {name}! " * times).strip()
# Optional and Union
from typing import Optional, Union
def find(id: int) -> Optional[str]:
return "Alice" if id == 1 else None
# mypy for static type checking
# $ mypy script.pyConversión de Tipos
Python tiene funciones de conversión integradas: int(), float(), str(), bool(), list(), tuple(), set(), dict(). Los valores falsos incluyen 0, '', [], {}, None, False. int() trunca hacia cero, mientras round() usa redondeo bancario.
# String to number
num_str = str(42) # "42"
num = int("42") # 42
float_num = float("3.14") # 3.14
# Number conversions
print(int(3.99)) # 3 (truncates toward zero)
print(int(-3.99)) # -3
print(round(3.14159, 2)) # 3.14
# Boolean conversion
print(bool(0)) # False
print(bool("")) # False
print(bool([])) # False
print(bool("anything")) # True
# Collection conversions
print(list("abc")) # ['a', 'b', 'c']
print(tuple([1, 2, 3])) # (1, 2, 3)
print(set([1, 1, 2])) # {1, 2}Tipos Numéricos
Los enteros de Python tienen precisión arbitraria (sin desbordamiento). Los floats son doubles IEEE 754 con los problemas de precisión habituales. Los números complejos están integrados. Los booleanos son una subclase de int (True==1, False==0). Usa guiones bajos en literales numéricos para legibilidad.
# Integers (arbitrary precision)
big = 10 ** 100 # no overflow
print(type(big)) # <class 'int'>
# Floats (IEEE 754 double)
pi = 3.14159
print(0.1 + 0.2) # 0.30000000000000004
# Complex numbers
z = 3 + 4j
print(z.real, z.imag) # 3.0 4.0
print(abs(z)) # 5.0
# Boolean is subclass of int
print(isinstance(True, int)) # True
print(True + True) # 2
# Underscores in numbers (3.6+)
million = 1_000_000
binary = 0b_1010_1010Constantes y Convenciones de Nombres
Python no tiene palabra clave const: los nombres ALL_CAPS son constantes solo por convención (nada impide la reasignación). PEP 8 define el nombrado: snake_case para variables/funciones, PascalCase para clases, ALL_CAPS para constantes. El guión bajo inicial significa 'privado' por convención; el doble guión bajo activa el name mangling.
# Python has no true constants - convention only
MAX_SIZE = 100 # ALL_CAPS for constants
PI = 3.14159
# Naming conventions (PEP 8)
variable_name = "snake_case" # variables, functions
ClassName = "PascalCase" # classes
CONSTANT_VALUE = 100 # constants
_private_var = "underscore prefix" # private (convention)
__name_mangled = "double underscore" # name mangling
# dunder names (reserved)
__name__, __main__, __init__Cadenas
Métodos de Cadena
Las cadenas son inmutables: los métodos devuelven nuevas cadenas. find() devuelve -1 si no se encuentra, mientras index() lanza ValueError. Usa isalpha()/isdigit()/isalnum() para validación. La clase str tiene más de 40 métodos: explora con dir(str).
s = "Hello, World"
# Case operations
print(s.upper()) # HELLO, WORLD
print(s.lower()) # hello, world
print(s.title()) # Hello, World
print(s.capitalize()) # Hello, world
print(s.swapcase()) # hELLO, wORLD
# Search & replace
print(s.find("World")) # 7 (index, -1 if not found)
print(s.index("World")) # 7 (raises ValueError if not found)
print(s.replace("o", "0")) # Hell0, W0rld
print(s.count("l")) # 3
# Validation
print("abc".isalpha()) # True
print("123".isdigit()) # True
print(" ".isspace()) # TrueFormateo de Cadenas
Los f-strings son la forma moderna, más rápida y legible de formatear cadenas. Admiten especificaciones de formato tras dos puntos: :.2f para 2 decimales, :>10 para alinear a la derecha con ancho 10, :, para separador de miles. Evita el formateo con % en código nuevo.
name = "Alice"
age = 30
# f-strings (Python 3.6+) - PREFERRED
print(f"Hello, {name}! You are {age}.")
print(f"{name.upper()} is {age * 365} days old")
print(f"{3.14159:.2f}") # 3.14
print(f"{42:>10}") # right-align
print(f"{42:<10}") # left-align
print(f"{42:^10}") # center
print(f"{1000000:,}") # 1,000,000
# str.format() method
print("Hello, {}!".format(name))
print("{name} is {age}".format(name="Bob", age=25))
# Old style (avoid in new code)
print("Hello, %s!" % name)Slicing e Indexación
La sintaxis de slicing de Python [start:stop:step] es potente: stop es exclusivo. Los índices negativos cuentan desde el final. s[::-1] es la forma idiomática de invertir una cadena. El slicing es seguro: los índices fuera de rango devuelven cadenas vacías en lugar de lanzar errores.
s = "Hello, World"
# Indexing (0-based, negative from end)
print(s[0]) # H
print(s[-1]) # d
print(s[7]) # W
# Slicing [start:stop:step]
print(s[0:5]) # Hello
print(s[7:]) # World
print(s[:5]) # Hello
print(s[::2]) # HloWrd (every 2nd char)
print(s[::-1]) # dlroW ,olleH (reverse!)
# Length
print(len(s)) # 12
# Slicing never raises IndexError
print(s[100:200]) # '' (empty string)División y Unión
split() divide una cadena en una lista, join() combina una lista en una cadena. Usa siempre join() para concatenar eficientemente muchas cadenas: el operador + crea cadenas intermedias. partition() divide en exactamente 3 partes (antes, separador, después).
# Split
csv = "a,b,c,d"
print(csv.split(",")) # ['a', 'b', 'c', 'd']
print(csv.split(",", 2)) # ['a', 'b', 'c,d'] (max 2 splits)
# Splitlines
text = "line1\nline2\nline3"
print(text.splitlines()) # ['line1', 'line2', 'line3']
# Partition (splits on first occurrence)
print("[email protected]".partition("@"))
# ('user', '@', 'domain.com')
# Join
words = ["Hello", "World"]
print(" ".join(words)) # Hello World
print("-".join(["2024", "01", "15"])) # 2024-01-15
print("".join(["a", "b", "c"])) # abc
# String concatenation
s = "Hello" + " " + "World"
parts = ["a"]
parts += "b" # NOT string concat - adds chars to list!Recorte y Relleno
strip() elimina los espacios en blanco iniciales/finales por defecto, o los caracteres especificados. zfill() rellena con ceros a la izquierda (útil para IDs). rjust/ljust/center rellenan hasta un ancho especificado con un carácter de relleno opcional.
# Strip whitespace (or specified chars)
s = " hello "
print(s.strip()) # "hello"
print(s.lstrip()) # "hello "
print(s.rstrip()) # " hello"
# Strip specific characters
print("xxxhelloxxx".strip("x")) # hello
# Padding / centering
print("42".zfill(5)) # 00042
print("hi".rjust(10)) # " hi"
print("hi".ljust(10, "-")) # "hi--------"
print("hi".center(10, "*")) # "****hi****"
# expandtabs
print("a\tb".expandtabs(4)) # "a b"Cadenas Raw y Escapes
Las cadenas raw (r'...') tratan las barras invertidas literalmente: esenciales para patrones regex y rutas de archivos de Windows. Las cadenas con triple comilla preservan los saltos de línea. Las cadenas admiten el operador * para repetición y + para concatenación.
# Escape sequences
print("Line1\nLine2") # newline
print("Tab\there") # tab
print("Quote: \"hi\"") # escaped quotes
print("Backslash: \\") # literal backslash
# Raw strings (ignore escapes) - great for regex
path = r"C:\Users\name\file.txt"
regex = r"\d{3}-\d{4}"
print(path) # C:\Users\name\file.txt
# Triple-quoted strings
multi = """
Multiple
lines
"""
# String multiplication
print("ab" * 3) # abababNúmeros y Matemáticas
Operadores Aritméticos
Python tiene 7 operadores aritméticos. / siempre devuelve float, // es división entera (redondea hacia menos infinito). ** es exponentación (no ^, que es XOR). El resultado del operador % toma el signo del divisor, a diferencia de C/Java.
# Basic operators
print(7 + 3) # 10 addition
print(7 - 3) # 4 subtraction
print(7 * 3) # 21 multiplication
print(7 / 3) # 2.333... true division (always float)
print(7 // 3) # 2 floor division
print(7 % 3) # 1 modulo (remainder)
print(7 ** 3) # 343 exponentiation
# Floor division with negatives
print(-7 // 3) # -3 (rounds toward negative infinity)
print(-7 % 3) # 2 (result has same sign as divisor)
# Augmented assignment
x = 10
x += 5 # x = x + 5
x **= 2 # x = x ** 2Módulo math
El módulo math proporciona funciones y constantes matemáticas. Todas las funciones trigonométricas usan radianes: convierte con math.radians()/degrees(). math.gcd() encuentra el máximo común divisor. Para números complejos, usa el módulo cmath.
import math
# Constants
print(math.pi) # 3.141592653589793
print(math.e) # 2.718281828459045
print(math.inf) # inf
print(math.nan) # nan
# Functions
print(math.sqrt(16)) # 4.0
print(math.pow(2, 10)) # 1024.0
print(math.log(100, 10)) # 2.0 (log base 10)
print(math.log(math.e)) # 1.0 (natural log)
print(math.factorial(5)) # 120
print(math.gcd(12, 8)) # 4
# Rounding
print(math.floor(3.7)) # 3
print(math.ceil(3.2)) # 4
print(math.trunc(-3.7)) # -3 (toward zero)
# Trigonometry (radians)
print(math.sin(math.pi / 2)) # 1.0
print(math.degrees(math.pi)) # 180.0Módulo random
El módulo random usa el PRNG Mersenne Twister: NO es criptográficamente seguro. Usa el módulo secrets para seguridad. random.sample() elige elementos únicos, random.choices() admite duplicados. Establece una semilla para resultados reproducibles en pruebas.
import random
# Random integers
print(random.randint(1, 100)) # 1 to 100 inclusive
print(random.randrange(0, 10, 2)) # even number 0,2,4,6,8
# Random floats
print(random.random()) # 0.0 to 1.0
print(random.uniform(1.0, 10.0)) # random float in range
# Choice & sampling
colors = ["red", "green", "blue"]
print(random.choice(colors)) # one random item
print(random.sample(colors, 2)) # 2 unique items
print(random.choices(colors, k=5)) # 5 items (with replacement)
# Shuffle (in-place)
nums = [1, 2, 3, 4, 5]
random.shuffle(nums)
print(nums)
# Reproducible randomness
random.seed(42) # same seed = same sequenceDecimal y Fractions
Usa Decimal para cálculos financieros donde los errores de precisión de float son inaceptables (por ejemplo, dinero). Usa Fraction para aritmética racional exacta. Ambos son más lentos que float pero evitan errores de redondeo. Construye siempre Decimal a partir de cadenas, no de floats.
from decimal import Decimal, getcontext
from fractions import Fraction
# Float precision issues
print(0.1 + 0.2) # 0.30000000000000004
# Decimal for exact decimal arithmetic
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b) # 0.3 (exact!)
# Set precision
getcontext().prec = 6
print(Decimal(1) / Decimal(7)) # 0.142857
# Fractions for exact rational arithmetic
f1 = Fraction(1, 3)
f2 = Fraction(1, 6)
print(f1 + f2) # 1/2
print(float(f1)) # 0.3333...
# Fraction from string
print(Fraction("3/4")) # 3/4Operadores a Nivel de Bits
Los operadores a nivel de bits manipulan bits individuales de enteros. Los enteros de Python tienen precisión arbitraria, por lo que los desplazamientos funcionan de forma diferente que en lenguajes de ancho fijo. Usos comunes: flags, máscaras, análisis de protocolos de bajo nivel. x & (x-1) == 0 comprueba si x es potencia de 2.
# Bitwise operators work on integers
a = 0b1010 # 10
b = 0b1100 # 12
print(a & b) # 8 (0b1000) AND
print(a | b) # 14 (0b1110) OR
print(a ^ b) # 6 (0b0110) XOR
print(~a) # -11 (NOT, two's complement)
print(a << 2) # 40 (left shift, multiply by 4)
print(a >> 1) # 5 (right shift, divide by 2)
# Binary representation
print(bin(10)) # 0b1010
print(hex(255)) # 0xff
print(oct(8)) # 0o10
print(int("1010", 2)) # 10 (parse binary)
# Common tricks
print(5 & 1) # 1 (check odd: nonzero = odd)
print(8 & (8-1)) # 0 (check power of 2)Estructuras de Datos
Listas
Las listas son la estructura de datos más versátil de Python: ordenadas, mutables y heterogéneas. append() es O(1), insert(0, x) es O(n). Usa collections.deque para operaciones rápidas en ambos extremos. sort() es in-place, sorted() devuelve una lista nueva.
# Lists are ordered, mutable sequences
nums = [1, 2, 3, 4, 5]
mixed = [1, "hello", True, 3.14]
# Adding elements
nums.append(6) # [1,2,3,4,5,6]
nums.insert(0, 0) # [0,1,2,3,4,5,6]
nums.extend([7, 8]) # extend with another list
# Removing elements
nums.remove(0) # remove by value
popped = nums.pop() # remove & return last
popped = nums.pop(0) # remove & return by index
del nums[0] # delete by index
nums.clear() # remove all
# Slicing (same as strings)
nums = [1, 2, 3, 4, 5]
print(nums[1:3]) # [2, 3]
print(nums[::-1]) # [5, 4, 3, 2, 1] reverse
# Sorting
nums.sort() # in-place sort
nums.sort(reverse=True) # descending
sorted_nums = sorted(nums) # returns new listTuplas
Las tuplas son inmutables y más rápidas que las listas. Úsalas para colecciones fijas, múltiples valores de retorno y claves de diccionario (las listas no pueden ser claves). Las named tuples proporcionan nombres de campo para legibilidad. Las tuplas de un solo elemento necesitan una coma final.
# Tuples are ordered, IMMUTABLE sequences
point = (3, 4)
single = (42,) # note the comma for single-element tuple
empty = ()
# Packing & unpacking
coordinates = 10, 20, 30 # packing
x, y, z = coordinates # unpacking
x, y = y, x # swap values!
# Multiple return values
def min_max(nums):
return min(nums), max(nums)
lo, hi = min_max([3, 1, 4, 1, 5])
# Named tuples (readable)
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p[0], p[1]) # 3 4
# Tuples are immutable but can contain mutable objects
t = (1, [2, 3])
t[1].append(4) # OK: (1, [2, 3, 4])Diccionarios
Los diccionarios son hash maps: O(1) promedio para búsqueda/inserción/borrado. Las claves deben ser hashable (inmutables). Desde Python 3.7, los dicts mantienen el orden de inserción. Usa get() para evitar KeyError. La dict comprehension crea dicts de forma elegante.
# Dicts are key-value mappings (insertion-ordered since 3.7)
user = {"name": "Alice", "age": 30}
# Access
print(user["name"]) # Alice
print(user.get("email")) # None (no KeyError)
print(user.get("email", "N/A")) # N/A (default)
# Add/update
user["email"] = "[email protected]" # add
user["age"] = 31 # update
user.setdefault("role", "user") # set if missing
# Delete
del user["email"]
val = user.pop("age") # remove & return
# user.clear() # remove all
# Iteration
for key in user: # keys
print(key)
for k, v in user.items(): # key-value pairs
print(k, v)
for v in user.values(): # values
print(v)
# Dict comprehension
squares = {x: x**2 for x in range(5)}
# Merge dicts (3.9+)
merged = {"a": 1} | {"b": 2}Conjuntos
Los conjuntos son colecciones no ordenadas de elementos únicos y hashable. Destacan en pruebas de pertenencia (O(1) frente a O(n) en listas) y álgebra de conjuntos (unión, intersección, diferencia). frozenset es inmutable y hashable. El orden no está garantizado.
# Sets are unordered collections of unique elements
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# Set operations
print(a | b) # union: {1, 2, 3, 4, 5, 6}
print(a & b) # intersection: {3, 4}
print(a - b) # difference: {1, 2}
print(a ^ b) # symmetric difference: {1, 2, 5, 6}
# Methods
a.add(5) # add element
a.discard(10) # remove if present (no error)
a.remove(1) # remove (KeyError if missing)
a.update([6, 7]) # add multiple
# Membership test (O(1) - faster than list)
print(3 in a) # True
# Frozen set (immutable)
fs = frozenset([1, 2, 3])
# Common use: deduplicate
unique = list(set([1, 1, 2, 2, 3])) # [1, 2, 3]Comprensiones
Las comprensiones son una forma pitónica de crear colecciones de manera concisa. Las expresiones generadoras (paréntesis en lugar de corchetes) son perezosas: producen valores bajo demanda, ahorrando memoria. Prefiere comprensiones sobre map()/filter() por legibilidad.
# List comprehension
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
pairs = [(x, y) for x in range(3) for y in range(3)]
# Dict comprehension
square_map = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Set comprehension
unique_lens = {len(w) for w in ["a", "ab", "abc", "ab"]}
# Generator expression (lazy, memory-efficient)
gen = (x**2 for x in range(1000000))
print(next(gen)) # 0
print(next(gen)) # 1
total = sum(x**2 for x in range(100)) # no extra list
# Nested comprehension (matrix)
matrix = [[i * 3 + j for j in range(3)] for i in range(3)]
# [[0,1,2], [3,4,5], [6,7,8]]Módulo collections
El módulo collections proporciona contenedores especializados. Counter cuenta elementos hashable. defaultdict crea automáticamente las claves ausentes. deque ofrece append/pop en O(1) en ambos extremos (frente a O(n) en listas). Son esenciales para código limpio y eficiente.
from collections import Counter, defaultdict, deque, OrderedDict
# Counter - counting
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
cnt = Counter(words)
print(cnt) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(cnt.most_common(2)) # [('apple', 3), ('banana', 2)]
# defaultdict - no KeyError
dd = defaultdict(list)
dd["fruits"].append("apple")
dd["fruits"].append("banana")
# dd["vegs"] automatically creates empty list
# deque - fast double-ended queue
dq = deque([1, 2, 3])
dq.appendleft(0) # [0, 1, 2, 3]
dq.append(4) # [0, 1, 2, 3, 4]
dq.popleft() # 0, deque is now [1, 2, 3, 4]
dq.rotate(1) # rotate right
# OrderedDict (less needed since 3.7, dicts are ordered)
od = OrderedDict([("a", 1), ("b", 2)])Flujo de Control
If / Elif / Else
Python usa if/elif/else: observa 'elif' no 'elseif'. La indentación define los bloques. El operador ternario 'x if cond else y' es una expresión. Python trata las colecciones vacías, 0, None y False como falsos: útil para condicionales concisos.
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
print(f"Grade: {grade}") # Grade: B
# Conditional expression (ternary)
status = "pass" if score >= 60 else "fail"
# Truthy/falsy values
# Falsy: False, 0, 0.0, "", [], {}, (), None
# Everything else is truthy
if []: # False
print("never")
if [0]: # True (non-empty list)
print("always")Bucles for e Iteración
El bucle for de Python itera sobre cualquier iterable. range() genera números (stop exclusivo). enumerate() empareja elementos con índices. zip() itera varias secuencias en paralelo. Usa .items() para iterar pares clave-valor de un dict.
# range(start, stop, step)
for i in range(5): # 0, 1, 2, 3, 4
print(i)
for i in range(2, 10, 2): # 2, 4, 6, 8
print(i)
for i in range(10, 0, -1): # countdown
print(i)
# Iterate over collections
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# enumerate for index + value
for idx, fruit in enumerate(fruits):
print(f"{idx}: {fruit}")
# zip to iterate multiple sequences
names = ["Alice", "Bob"]
ages = [30, 25]
for name, age in zip(names, ages):
print(f"{name}: {age}")
# Iterate dict
user = {"name": "Alice", "age": 30}
for key, value in user.items():
print(f"{key} = {value}")Bucles while y Break/Continue
Los bucles while se repiten mientras una condición sea verdadera. break sale del bucle inmediatamente, continue salta a la siguiente iteración. La construcción for/else ejecuta el bloque else solo si el bucle no se rompió. pass es un marcador de no operación para bloques vacíos.
# Basic while
count = 0
while count < 5:
print(count)
count += 1
# break - exit loop
while True:
cmd = input("> ")
if cmd == "quit":
break
print(f"You said: {cmd}")
# continue - skip to next iteration
for i in range(10):
if i % 2 == 0:
continue # skip even numbers
print(i) # prints 1, 3, 5, 7, 9
# else clause (runs if no break)
for i in range(5):
if i == 10:
break
else:
print("Loop completed without break")
# pass - do nothing (placeholder)
for i in range(5):
pass # TODO: implementSentencia match (Python 3.10+)
La sentencia match (Python 3.10+) es un potente emparejamiento estructural de patrones, mucho más allá del switch de C. Puede emparejar secuencias, mapeos, instancias de clases y vincular variables. El patrón _ es un comodín (por defecto). Las guardas con 'if' añaden condiciones.
# Structural pattern matching (like switch)
def handle_command(cmd):
match cmd.split():
case ["quit"]:
return "Goodbye"
case ["hello", name]:
return f"Hello, {name}!"
case ["move", direction] if direction in "NSEW":
return f"Moving {direction}"
case ["add", x, y]:
return int(x) + int(y)
case _:
return "Unknown command"
print(handle_command("hello Alice")) # Hello, Alice!
# Matching data structures
match point:
case (0, 0):
print("origin")
case (0, y):
print(f"on y-axis at {y}")
case (x, 0):
print(f"on x-axis at {x}")
case (x, y):
print(f"at ({x}, {y})")Iteradores y Generadores
Los generadores producen valores de forma perezosa usando yield: no calculan todos los valores por adelantado, ahorrando memoria. Implementan el protocolo iterador (iter() y next()). Una vez agotados, terminan. Usa generadores para secuencias grandes/infinitas, pipelines y datos en streaming.
# Iterator protocol
nums = [1, 2, 3]
it = iter(nums)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# next(it) # StopIteration
# Generator function (uses yield)
def count_up_to(n):
count = 1
while count <= n:
yield count
count += 1
for num in count_up_to(5):
print(num) # 1, 2, 3, 4, 5
# Infinite generator
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
# Generator expression
squares = (x**2 for x in range(10))Funciones
Definir y Llamar Funciones
Las funciones se definen con def. Los argumentos por defecto usan =. Python admite argumentos con nombre para mayor claridad. Las funciones pueden devolver múltiples valores (como tupla). Los docstrings (entre triple comilla) documentan funciones y son accesibles vía help().
# Basic function
def greet(name):
return f"Hello, {name}!"
print(greet("Alice")) # Hello, Alice!
# Default arguments
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Bob")) # Hello, Bob!
print(greet("Bob", "Hi")) # Hi, Bob!
# Keyword arguments
print(greet(name="Carol", greeting="Hey"))
# Return multiple values (tuple)
def stats(nums):
return min(nums), max(nums), sum(nums) / len(nums)
lo, hi, avg = stats([1, 2, 3, 4, 5])
# Docstrings
def add(a, b):
"""Add two numbers and return the result.
Args:
a: First number
b: Second number
Returns:
Sum of a and b
"""
return a + bArgumentos: *args y **kwargs
*args recoge argumentos posicionales extra en una tupla, **kwargs recoge argumentos con nombre extra en un dict. Los nombres args/kwargs son por convención. Puedes desempaquetar secuencias con * y dicts con ** al llamar funciones. Orden: posicionales, *args, con nombre, **kwargs.
# *args - variable positional arguments (tuple)
def sum_all(*args):
return sum(args)
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
# **kwargs - variable keyword arguments (dict)
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=30, role="admin")
# Combining all
def func(a, b, *args, **kwargs):
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"kwargs={kwargs}")
func(1, 2, 3, 4, x=5, y=6)
# a=1, b=2, args=(3, 4), kwargs={'x': 5, 'y': 6}
# Unpacking arguments
nums = [1, 2, 3]
print(sum_all(*nums)) # unpack list as args
opts = {"name": "Alice", "age": 30}
print_info(**opts) # unpack dict as kwargsLambda y Funciones de Orden Superior
Las lambdas se limitan a una sola expresión: usa def para lógica compleja. Brillian como argumentos de funciones de orden superior como sorted(), map(), filter(). Sin embargo, las list comprehensions suelen ser más legibles que map/filter. reduce() vive en functools.
# Lambda - anonymous function (single expression)
square = lambda x: x ** 2
print(square(5)) # 25
# Common with sorted, map, filter, reduce
students = [("Alice", 85), ("Bob", 92), ("Carol", 78)]
# Sort by score (key function)
sorted_by_score = sorted(students, key=lambda s: s[1])
# [('Carol', 78), ('Alice', 85), ('Bob', 92)]
# map - apply function to each item
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
# [2, 4, 6, 8, 10]
# filter - keep items where function returns True
evens = list(filter(lambda x: x % 2 == 0, nums))
# [2, 4]
# reduce - accumulate to single value
from functools import reduce
product = reduce(lambda a, b: a * b, nums)
# 120 (1*2*3*4*5)
# Prefer comprehensions over map/filter
doubled = [x * 2 for x in nums] # more Pythonic
evens = [x for x in nums if x % 2 == 0]Decoradores
Los decoradores envuelven funciones para añadir comportamiento sin modificar el código original. La sintaxis @ es azúcar sintáctico. Los decoradores con argumentos necesitan un nivel extra de anidamiento. Usa siempre functools.wraps para preservar los metadatos de la función original (nombre, docstring).
# A decorator modifies a function's behavior
def uppercase_result(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase_result
def greet(name):
return f"hello, {name}"
print(greet("alice")) # HELLO, ALICE
# Decorator with arguments
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
say_hi() # prints "Hi!" three times
# Practical: timing decorator
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
# Use functools.wraps to preserve metadata
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapperÁmbito y Closures
Python resuelve nombres usando el orden de ámbito LEGB: Local, Enclosing, Global, Built-in. Usa 'global' para reasignar una variable global dentro de una función. Usa 'nonlocal' (Python 3) para modificar una variable en un ámbito envolvente. Las closures recuerdan su ámbito envolvente.
# LEGB rule: Local, Enclosing, Global, Built-in
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
# global keyword - modify global variable
count = 0
def increment():
global count
count += 1
# nonlocal keyword - modify enclosing variable
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3POO y Clases
Clases y Objetos
Las clases agrupan datos (atributos) y comportamiento (métodos). __init__ es el constructor. self se refiere a la instancia (como 'this' en otros lenguajes). Las variables de clase son compartidas; las variables de instancia son por objeto. __str__ es para usuarios, __repr__ es para desarrolladores.
class Dog:
# Class variable (shared by all instances)
species = "Canis familiaris"
# Constructor
def __init__(self, name, age):
# Instance variables
self.name = name
self.age = age
# Instance method
def bark(self):
return f"{self.name} says Woof!"
# String representation
def __str__(self):
return f"Dog({self.name}, {self.age})"
# Official representation (for debugging)
def __repr__(self):
return f"Dog(name='{self.name}', age={self.age})"
# Create instances
buddy = Dog("Buddy", 3)
lucy = Dog("Lucy", 5)
print(buddy.bark()) # Buddy says Woof!
print(buddy.name) # Buddy
print(buddy.species) # Canis familiaris
print(str(buddy)) # Dog(Buddy, 3)Herencia y Polimorfismo
La herencia permite a las clases reutilizar y extender comportamiento. Python admite herencia múltiple con MRO (Method Resolution Order) para resolver conflictos. El polimorfismo permite tratar diferentes tipos de forma uniforme. Usa isinstance() para comprobación de tipos, no type().
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement")
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
# Polymorphism - same interface, different behavior
def animal_sound(animal):
print(animal.speak())
animals = [Dog("Buddy"), Cat("Whiskers")]
for a in animals:
animal_sound(a)
# Buddy says Woof!
# Whiskers says Meow!
# Multiple inheritance
class Swimmer:
def swim(self):
return "swimming"
class Flyer:
def fly(self):
return "flying"
class Duck(Animal, Swimmer, Flyer):
pass
duck = Duck("Donald")
print(duck.swim()) # swimming
print(duck.fly()) # flying
# Check inheritance
print(isinstance(duck, Animal)) # True
print(issubclass(Dog, Animal)) # TruePropiedades y Encapsulación
Python no tiene privado/protected real: usa convenciones. Un solo guión bajo _ significa 'interno'. El doble guión bajo __ activa name mangling (no privacidad real). @property convierte métodos en atributos con getters/setters, habilitando validación y propiedades calculadas.
class Temperature:
def __init__(self, celsius=0):
self.celsius = celsius # uses setter below
# Getter
@property
def celsius(self):
return self._celsius
# Setter
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
# Computed property
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
temp = Temperature(25)
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 100
print(temp.celsius) # 37.78...
# Name conventions:
# _name - protected (convention, not enforced)
# __name - private (name mangling: _ClassName__name)
# __name__ - dunder (reserved by Python)Métodos de Clase y Estáticos
@staticmethod es solo una función en el espacio de nombres de la clase: sin primer argumento implícito. @classmethod recibe la clase (cls) como primer argumento, útil para constructores alternativos (métodos factory) y comportamiento consciente de la herencia. Usa classmethod para constructores, staticmethod para funciones utilitarias.
class MathUtils:
pi = 3.14159
# Static method - no self/cls, lives in class namespace
@staticmethod
def add(a, b):
return a + b
# Class method - receives the class as first argument
@classmethod
def circle_area(cls, radius):
return cls.pi * radius ** 2
# Alternative constructor (common classmethod use)
@classmethod
def from_diameter(cls, diameter):
return cls() # would configure instance
# Static: called on class or instance, no special first arg
print(MathUtils.add(2, 3)) # 5
# Class: often used for alternative constructors
print(MathUtils.circle_area(5)) # 78.54...
# Factory pattern with classmethod
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@classmethod
def origin(cls):
return cls(0, 0)
@classmethod
def from_tuple(cls, coords):
return cls(*coords)
p1 = Point.origin()
p2 = Point.from_tuple((3, 4))Métodos Mágicos (Dunder)
Los métodos mágicos (métodos dunder) implementan la sobrecarga de operadores y el comportamiento de protocolos. __add__ para +, __eq__ para ==, __len__ para len(), __iter__ para iteración/desempaquetado. Permiten que tus objetos funcionen con la sintaxis y funciones integradas de Python de forma natural.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
# String representation
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Operator overloading
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
# Equality
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# Length
def __len__(self):
return int((self.x**2 + self.y**2) ** 0.5)
# Make it iterable
def __iter__(self):
yield self.x
yield self.y
# Index access
def __getitem__(self, index):
return (self.x, self.y)[index]
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2) # Vector(6, 8)
print(v1 * 3) # Vector(6, 9)
print(v1 == Vector(2, 3)) # True
print(len(v1)) # 3
x, y = v1 # unpacking via __iter__Manejo de Errores
Try / Except / Finally
try/except/else/finally: try ejecuta código arriesgado, except captura errores, else se ejecuta si no hay excepción, finally siempre se ejecuta (limpieza). Captura excepciones específicas, no 'except:' desnudo. El bloque else es útil cuando la limpieza solo debe ocurrir en caso de éxito. Exception es la base de la mayoría de errores capturables.
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}") # division by zero
finally:
print("This always runs")
# Multiple exception types
try:
value = int("abc")
except (ValueError, TypeError) as e:
print(f"Conversion error: {e}")
# Different handlers for different exceptions
try:
f = open("nonexistent.txt")
data = f.read()
except FileNotFoundError:
print("File not found")
except PermissionError:
print("No permission")
except Exception as e:
print(f"Unexpected: {e}")
else:
print("No exception occurred")
f.close()
finally:
print("Cleanup (always runs)")
# Exception hierarchy
# BaseException
# ├── SystemExit
# ├── KeyboardInterrupt
# └── Exception
# ├── ValueError
# ├── TypeError
# ├── KeyError
# └── ...Lanzar Excepciones
Usa raise para lanzar excepciones. 'raise' solo relanza la excepción actual (en un bloque except). 'raise X from Y' encadena excepciones, preservando la causa original. Lanza siempre tipos de excepción específicos. Evita usar excepciones para el flujo de control normal.
# Raise an exception
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero!")
return a / b
# Re-raise the current exception
def process(data):
try:
return parse(data)
except ValueError:
print("Logging parse error...")
raise # re-raises the same exception
# Raise with context (from)
try:
int("abc")
except ValueError as e:
raise RuntimeError("Failed to process input") from e
# Common built-in exceptions
raise ValueError("invalid value")
raise TypeError("wrong type")
raise KeyError("missing key")
raise IndexError("out of range")
raise RuntimeError("something went wrong")
raise NotImplementedError("override this")
raise FileNotFoundError("no such file")
# Exception with custom args
class ValidationError(Exception):
pass
raise ValidationError("field is required", "email")Excepciones Personalizadas
Crea excepciones personalizadas heredando de Exception (o de una integrada más específica). Diseña una jerarquía para que los llamadores puedan capturar al nivel adecuado. Añade atributos personalizados para transportar contexto. Hereda de Exception, no de BaseException (que incluye SystemExit/KeyboardInterrupt).
# Custom exception hierarchy
class AppError(Exception):
"""Base exception for the application."""
pass
class DatabaseError(AppError):
def __init__(self, message, query=None):
super().__init__(message)
self.query = query
class ValidationError(AppError):
def __init__(self, field, message):
super().__init__(f"{field}: {message}")
self.field = field
self.message = message
class AuthenticationError(AppError):
pass
# Usage
def login(username, password):
if not username:
raise ValidationError("username", "is required")
if password != "secret":
raise AuthenticationError("Invalid credentials")
# Catching by hierarchy
try:
login("", "x")
except ValidationError as e:
print(f"Validation failed: {e.field}")
except AppError as e:
print(f"App error: {e}")
# Access exception info
import traceback
try:
1 / 0
except:
traceback.print_exc()
print(repr(sys.exc_info()[1]))Gestores de Contexto (sentencia with)
Los gestores de contexto (la sentencia 'with') garantizan la limpieza vía __enter__ y __exit__. Son esenciales para recursos como archivos, locks y conexiones a bases de datos. contextlib.contextmanager simplifica la creación usando un generador. __exit__ puede suprimir excepciones devolviendo True.
# Context managers handle setup and cleanup
with open("file.txt") as f:
content = f.read()
# file is automatically closed, even if an error occurs
# Multiple context managers
with open("input.txt") as fin, open("output.txt", "w") as fout:
fout.write(fin.read())
# Creating a context manager (class-based)
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.time() - self.start
print(f"Elapsed: {self.elapsed:.4f}s")
return False # don't suppress exceptions
with Timer() as t:
# code to time
sum(range(1000000))
# contextlib for simpler context managers
from contextlib import contextmanager
@contextmanager
def open_db(url):
db = connect(url)
try:
yield db
finally:
db.close()
with open_db("localhost") as db:
db.query("SELECT 1")Aserciones y Logging
Las sentencias assert son para depurar invariantes: se eliminan cuando Python se ejecuta con -O (optimizar). Nunca uses aserciones para validación de entrada. Usa el módulo logging en lugar de print() para código de producción: admite niveles, formateo y destinos de salida.
# Assertions - for debugging (removed with -O flag)
def divide(a, b):
assert b != 0, "Divisor cannot be zero"
return a / b
# Never use assertions for data validation (they can be disabled)
# Use them for invariant checks during development
# Logging (better than print for production)
import logging
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)
logger.debug("Detailed info for debugging")
logger.info("Confirmation things are working")
logger.warning("Something unexpected happened")
logger.error("A serious problem")
logger.critical("A fatal error")
# Log exceptions with traceback
try:
1 / 0
except:
logger.exception("Failed to divide") # includes tracebackE/S de Archivos
Leer Archivos
Usa siempre 'with' para abrir archivos: los cierra automáticamente aunque ocurra un error. Especifica encoding='utf-8' para evitar problemas de codificación dependientes de la plataforma. Para archivos grandes, itera línea a línea en lugar de read() para ahorrar memoria. readlines() carga todo el archivo en memoria.
# Read entire file
with open("file.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# Read line by line (memory-efficient for large files)
with open("file.txt", "r") as f:
for line in f:
print(line.strip()) # strip removes trailing newline
# Read all lines into a list
with open("file.txt") as f:
lines = f.readlines() # ['line1\n', 'line2\n', ...]
# Read specific number of characters
with open("file.txt") as f:
chunk = f.read(100) # first 100 chars
# File modes:
# "r" read (default)
# "w" write (truncate)
# "a" append
# "x" exclusive create (fails if exists)
# "b" binary mode (e.g., "rb", "wb")
# "+" read and write (e.g., "r+")Escribir Archivos
El modo 'w' trunca el archivo (borra el contenido); usa 'a' para añadir. writelines() no añade saltos de línea: añádelos manualmente. Usa 'rb'/'wb' para archivos binarios (imágenes, etc.). seek() mueve el cursor; tell() devuelve su posición. Especifica siempre la codificación para archivos de texto.
# Write text (overwrites existing)
with open("output.txt", "w") as f:
f.write("First line\n")
f.write("Second line\n")
# writelines doesn't add newlines
f.writelines(["line3\n", "line4\n"])
# Append to a file
with open("log.txt", "a") as f:
f.write("New log entry\n")
# Write binary data
with open("data.bin", "wb") as f:
f.write(b"\x00\x01\x02\x03")
# Read and write simultaneously
with open("file.txt", "r+") as f:
content = f.read()
f.seek(0) # move to beginning
f.write("Updated") # overwrite
f.truncate() # cut off remaining
# Check if file exists
import os
if os.path.exists("file.txt"):
print("File exists")Manejo de Rutas (pathlib)
pathlib (Python 3.4+) es la forma moderna y orientada a objetos de manejar rutas: prefiérelo sobre os.path. El operador / une rutas de forma independiente de la plataforma. Los objetos Path tienen métodos read_text()/write_text() que gestionan open/close por ti. rglob() busca recursivamente.
from pathlib import Path
# Create Path objects (preferred over os.path)
p = Path("src/main.py")
home = Path.home() # /home/user or C:\Users\user
cwd = Path.cwd() # current working directory
# Path components
print(p.name) # main.py
print(p.stem) # main
print(p.suffix) # .py
print(p.parent) # src
print(p.parts) # ('src', 'main.py')
# Joining paths (use / operator)
config = home / ".config" / "app" / "config.json"
# Existence and type
print(p.exists()) # True/False
print(p.is_file())
print(p.is_dir())
# Listing directories
for f in Path(".").iterdir():
print(f)
# Glob patterns
for py_file in Path(".").rglob("*.py"):
print(py_file)
# Create directories
Path("new/dir").mkdir(parents=True, exist_ok=True)
# Read/write (Path methods)
content = Path("file.txt").read_text()
Path("output.txt").write_text("Hello!")JSON
json.dumps() serializa a cadena, json.loads() deserializa. dump()/load() trabajan con archivos. Usa indent para pretty-printing. Los objetos personalizados necesitan un serializador por defecto. JSON solo admite tipos básicos: usa default= para datetime y otros objetos complejos.
import json
# Python dict to JSON string
data = {"name": "Alice", "age": 30, "skills": ["Python", "SQL"]}
json_str = json.dumps(data, indent=2)
print(json_str)
# JSON string to Python dict
parsed = json.loads('{"name": "Bob", "active": true}')
print(parsed["name"]) # Bob
print(parsed["active"]) # True (Python bool)
# Write JSON to file
with open("data.json", "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# Read JSON from file
with open("data.json") as f:
loaded = json.load(f)
# Custom serialization (e.g., datetime)
from datetime import datetime
def json_default(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError
json.dumps({"time": datetime.now()}, default=json_default)
# Type mapping:
# JSON object <-> Python dict
# JSON array <-> Python list
# JSON string <-> Python str
# JSON number <-> Python int/float
# JSON boolean <-> Python bool
# JSON null <-> Python NoneCSV y Otros Formatos
El módulo csv maneja CSV con comillado correcto. Usa newline='' al abrir archivos CSV en Windows. DictReader/DictWriter trabajan con nombres de columnas. pickle puede serializar cualquier objeto Python, pero es específico de Python e inseguro: nunca deserialices datos de fuentes no confiables.
import csv
# Write CSV
with open("data.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "age", "city"])
writer.writerows([
["Alice", 30, "NYC"],
["Bob", 25, "LA"],
])
# Read CSV
with open("data.csv") as f:
reader = csv.reader(f)
header = next(reader) # first row
for row in reader:
print(row) # ['Alice', '30', 'NYC']
# DictReader/DictWriter (column access by name)
with open("data.csv") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["age"])
# Pickle (Python-specific, can store any object)
import pickle
with open("data.pkl", "wb") as f:
pickle.dump({"complex": [1, 2, {"a": 3}]}, f)
with open("data.pkl", "rb") as f:
obj = pickle.load(f)
# WARNING: pickle is insecure - never unpickle untrusted data!Módulos y Paquetes
Importar Módulos
Los imports traen módulos. 'import X' mantiene limpio el espacio de nombres. 'from X import Y' es conveniente pero puede causar colisiones de nombres. Los alias (import X as Y) son comunes para librerías con convenciones (np, pd). Evita 'from X import *': contamina el espacio de nombres.
# Import entire module
import math
print(math.sqrt(16))
# Import specific names
from datetime import datetime, timedelta
now = datetime.now()
# Import with alias
import numpy as np
import pandas as pd
# Import all names (discouraged - pollutes namespace)
# from os import *
# Conditional import (try/except)
try:
import cjson as json
except ImportError:
import json
# Check what's in a module
import os
print(dir(os)) # list all attributes
print(os.__file__) # module location
print(os.__name__) # module name
# Reload a module (during development)
import importlib
importlib.reload(my_module)Crear Módulos y Paquetes
Un módulo es un archivo .py; un paquete es un directorio con __init__.py. El archivo __init__.py puede estar vacío o configurar el paquete. __all__ en __init__.py controla lo que 'from package import *' exporta. Python moderno (3.3+) admite paquetes de espacio de nombres sin __init__.py.
# A module is just a .py file
# mymath.py
def add(a, b):
return a + b
PI = 3.14159
# A package is a directory with __init__.py
# mypackage/
# __init__.py
# module1.py
# module2.py
# subpackage/
# __init__.py
# module3.py
# __init__.py can be empty or contain package initialization
# mypackage/__init__.py
from .module1 import ClassA
from .module2 import func_b
__version__ = "1.0.0"
__all__ = ["ClassA", "func_b"]
# Using the package
from mypackage import ClassA
from mypackage.subpackage import module3
# __all__ controls 'from package import *'
# Without __all__, * imports only what's in __init__.py__name__ == '__main__'
El idiom if __name__ == '__main__' permite que un archivo sirva tanto como script como módulo. Cuando se ejecuta directamente, __name__ es '__main__'; cuando se importa, es el nombre del módulo. Este patrón es esencial para crear módulos reutilizables que también pueden ejecutarse solos.
# script.py
def main():
print("Running main")
def helper():
print("Helper function")
if __name__ == "__main__":
# This code only runs when the file is executed directly
# NOT when imported as a module
main()
# When you run: python script.py
# __name__ is "__main__" -> main() runs
# When you: import script
# __name__ is "script" -> main() does NOT run
# This lets the module be both a script and an importable library
# Common pattern for CLI tools
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--name", required=True)
args = parser.parse_args()
print(f"Hello, {args.name}!")
if __name__ == "__main__":
main()Lo Destacado de la Biblioteca Estándar
La biblioteca estándar de Python es enorme e incluye baterías. os/sys para interacción con el sistema, datetime para fechas, collections para contenedores especializados, itertools para herramientas de iteradores, functools para programación funcional. Explora la documentación en docs.python.org/3/library/.
# os - operating system interface
import os
os.getcwd() # current directory
os.listdir(".") # list files
os.environ.get("HOME") # environment variables
# sys - system-specific
import sys
sys.argv # command-line arguments
sys.exit(0) # exit with status code
sys.path # module search path
# datetime - date and time
from datetime import datetime, timedelta
now = datetime.now()
# collections - specialized containers
from collections import Counter, defaultdict, deque
# itertools - iterator tools
from itertools import chain, cycle, repeat, product
# functools - higher-order functions
from functools import lru_cache, reduce, partial
# typing - type hints
from typing import List, Dict, Optional, Union, Any
# pathlib - path handling
from pathlib import Path
# subprocess - run external commands
import subprocess
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)Pip y Entornos Virtuales
Usa siempre entornos virtuales para aislar las dependencias del proyecto. venv está integrado; las alternativas incluyen virtualenv, conda y uv. Fija versiones en requirements.txt para reproducibilidad. Nunca instales paquetes globalmente con --user o como root: usa un venv.
# Create a virtual environment
# $ python -m venv venv
# Activate it
# Windows: venv\Scripts\activate
# Unix: source venv/bin/activate
# Install packages
# $ pip install requests
# $ pip install requests==2.28.0
# $ pip install "requests>=2.25,<3.0"
# Install from requirements file
# $ pip install -r requirements.txt
# requirements.txt example:
# requests==2.31.0
# numpy>=1.21.0
# pandas~=2.0.0 # compatible release
# List installed packages
# $ pip list
# $ pip freeze > requirements.txt
# Uninstall
# $ pip uninstall requests
# Show package info
# $ pip show requests
# Modern alternative: uv (faster)
# $ uv pip install requestsFecha y Hora
Módulo datetime
El módulo datetime proporciona las clases date, time, datetime y timedelta. datetime.now() devuelve la hora local; usa datetime.now(timezone.utc) para UTC. weekday() devuelve 0-6 (Lun-Dom). Usa siempre datetimes conscientes de la zona horaria en producción para evitar ambigüedad.
from datetime import datetime, date, time, timedelta
# Current date and time
now = datetime.now() # local time
utc_now = datetime.utcnow() # UTC (deprecated in 3.12)
utc = datetime.now(timezone.utc) # preferred
# Current date
today = date.today()
# Create specific date/time
dt = datetime(2024, 1, 15, 10, 30, 0)
d = date(2024, 1, 15)
t = time(10, 30, 0)
# Access components
print(now.year, now.month, now.day)
print(now.hour, now.minute, now.second)
print(now.weekday()) # 0=Monday, 6=Sunday
# From timestamp
ts = 1705315200
dt = datetime.fromtimestamp(ts)
# To timestamp
print(datetime.now().timestamp())Formateo y Análisis
strftime (string format time) convierte datetime a cadena; strptime (string parse time) convierte cadena a datetime. El formato ISO 8601 (isoformat/fromisoformat) es la mejor opción para almacenar fechas: es inequívoco y ordenable. Memoriza los códigos comunes: %Y %m %d %H %M %S.
from datetime import datetime
# Format datetime to string (strftime)
dt = datetime(2024, 1, 15, 10, 30)
print(dt.strftime("%Y-%m-%d")) # 2024-01-15
print(dt.strftime("%Y/%m/%d %H:%M")) # 2024/01/15 10:30
print(dt.strftime("%B %d, %Y")) # January 15, 2024
print(dt.strftime("%A")) # Monday
# Parse string to datetime (strptime)
dt = datetime.strptime("2024-01-15", "%Y-%m-%d")
dt = datetime.strptime("15/01/2024 10:30", "%d/%m/%Y %H:%M")
# Common format codes:
# %Y year (2024) %m month (01)
# %d day (15) %H hour (14)
# %M minute (30) %S second (00)
# %B month name %b month abbrev
# %A weekday name %a weekday abbrev
# %I 12-hour %p AM/PM
# %j day of year %U week number
# ISO format (recommended for storage)
iso = dt.isoformat() # "2024-01-15T10:30:00"
dt = datetime.fromisoformat("2024-01-15T10:30:00")timedelta y Aritmética
timedelta representa una duración. Puedes sumar/restar timedeltas a datetimes y restar dos datetimes para obtener un timedelta. timedelta normaliza: days=1, hours=25 se convierte en days=2, hours=1. total_seconds() da toda la duración en segundos.
from datetime import datetime, timedelta
now = datetime.now()
# Add/subtract time
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)
in_2_hours = now + timedelta(hours=2)
in_90_days = now + timedelta(days=90)
# Difference between dates
date1 = datetime(2024, 1, 15)
date2 = datetime(2024, 6, 18)
diff = date2 - date1
print(diff.days) # 155
print(diff.total_seconds())
# timedelta components
td = timedelta(days=5, hours=3, minutes=30)
print(td.days) # 5
print(td.seconds) # 12600 (3h 30m in seconds)
print(td.total_seconds())
# Comparisons
if now > date1:
print("now is later")
# Business day calculation (using numpy)
# import numpy as np
# business_days = np.busday_count(date1.date(), date2.date())Zonas Horarias
Usa siempre datetimes conscientes de la zona horaria (Python 3.9+ ZoneInfo es preferible a pytz). Almacena fechas en UTC y convierte a hora local solo para mostrar. Los datetimes naive (sin tzinfo) causan bugs sutiles. ZoneInfo usa la base de datos de zonas horarias IANA, gestionando el horario de verano automáticamente.
from datetime import datetime, timezone, timedelta
# Timezone-aware datetime
utc_time = datetime.now(timezone.utc)
print(utc_time) # 2024-01-15 10:30:00+00:00
# Create a timezone (offset-based)
tz_ny = timezone(timedelta(hours=-5), "EST")
ny_time = datetime.now(tz_ny)
# Convert between timezones
utc_time = datetime.now(timezone.utc)
ny_time = utc_time.astimezone(timezone(timedelta(hours=-5)))
tokyo_time = utc_time.astimezone(timezone(timedelta(hours=9)))
# Use zoneinfo (Python 3.9+) for IANA timezones
from zoneinfo import ZoneInfo
tz = ZoneInfo("America/New_York")
dt = datetime.now(tz)
print(dt.tzname()) # EST or EDT
# Common IANA timezones:
# "UTC"
# "America/New_York", "America/Los_Angeles"
# "Europe/London", "Europe/Paris"
# "Asia/Tokyo", "Asia/Shanghai"
# "Australia/Sydney"
# Best practice: store UTC, convert for display
utc_stored = datetime.now(timezone.utc)
local_display = utc_stored.astimezone(ZoneInfo("Asia/Shanghai"))Expresiones Regulares
Fundamentos del módulo re
re.search() encuentra la primera coincidencia en cualquier parte; re.match() solo al inicio; re.fullmatch() requiere que toda la cadena coincida. Usa cadenas raw (r'...') para patrones y evitar problemas de escape de barras invertidas. Los objetos Match proporcionan group(), start(), end() y span().
import re
# re.search - find first match anywhere in string
m = re.search(r"\d{4}", "Order #2024 was placed")
if m:
print(m.group()) # 2024
print(m.start(), m.end()) # 9 13
# re.match - match at beginning of string
m = re.match(r"Hello", "Hello, World")
print(m.group()) # Hello
# re.fullmatch - entire string must match
m = re.fullmatch(r"\d+", "12345")
print(bool(m)) # True
# re.findall - all matches as list
emails = re.findall(r"\S+@\S+", text)
numbers = re.findall(r"\d+", "a1b22c333") # ['1', '22', '333']
# re.finditer - all matches as iterator (with positions)
for m in re.finditer(r"\w+", "Hello World"):
print(m.group(), m.span())
# Match object methods
m = re.search(r"(\w+)@(\w+)", "[email protected]")
print(m.group()) # user@domain (whole match)
print(m.group(1)) # user (first group)
print(m.group(2)) # domain (second group)
print(m.groups()) # ('user', 'domain')Sintaxis de Patrones
Sintaxis regex: [] para clases de caracteres, \d \w \s para conjuntos comunes, cuantificadores (* + ? {}) para repetición, ^ $ \b para anclas, () para grupos, | para alternancia. Usa cadenas raw (r'...') para que las barras invertidas sean literales. Los cuantificadores voraces coinciden lo máximo posible; añade ? para perezoso (por ejemplo, *?).
import re
# Character classes
re.findall(r"[aeiou]", "hello") # vowels
re.findall(r"[^aeiou]", "hello") # non-vowels
re.findall(r"[a-z]", "Hello123") # lowercase
re.findall(r"[A-Za-z0-9]", "Hi-1!") # alphanumeric
# Predefined classes
# . any char except newline
# \d digit [0-9] \D non-digit
# \w word char [a-zA-Z0-9_] \W non-word
# \s whitespace \S non-whitespace
# Quantifiers
# * 0 or more
# + 1 or more
# ? 0 or 1
# {n} exactly n
# {n,} n or more
# {n,m} between n and m
re.findall(r"\d{3}", "1234567") # ['123', '456']
re.findall(r"\d{2,4}", "12345678") # ['1234', '5678']
# Anchors
# ^ start of string $ end of string
# \b word boundary
re.findall(r"^\w+", "Hello World") # ['Hello']
re.findall(r"\b\w+\b", "hi there") # ['hi', 'there']
# Groups & alternation
re.findall(r"(cat|dog)", "cat and dog") # ['cat', 'dog']
re.findall(r"(\w+)@(\w+\.\w+)", "[email protected]")Sustitución y División
re.sub() reemplaza coincidencias: usa backreferences (\1, \2) para referenciar grupos, o una función para reemplazo dinámico. re.split() es más potente que str.split(): admite patrones regex. Los grupos de captura en el patrón se incluyen en el resultado. Usa re.subn() para obtener (resultado, conteo).
import re
# re.sub - replace matches
result = re.sub(r"\d+", "#", "a1b22c333")
# 'a#b#c#'
# Replace with count limit
result = re.sub(r"\d", "X", "a1b2c3", count=2)
# 'aXbXc3'
# Use backreferences in replacement
result = re.sub(r"(\w+)@(\w+)", r"\2.\1", "user@domain")
# 'domain.user'
# Use function as replacement
def upper(m):
return m.group().upper()
result = re.sub(r"\b[a-z]", upper, "hello world")
# 'Hello World' (capitalize first letter of each word)
# re.split - split by pattern
parts = re.split(r"[,;\s]+", "a, b; c d")
# ['a', 'b', 'c', 'd']
# Split with capture groups (keeps delimiters)
parts = re.split(r"([,;])", "a,b;c")
# ['a', ',', 'b', ';', 'c']
# Split with maxsplit
parts = re.split(r",", "a,b,c,d", maxsplit=2)
# ['a', 'b', 'c,d']Compilación y Flags
Compila patrones con re.compile() cuando los uses repetidamente: es más rápido. Los flags modifican el comportamiento: IGNORECASE, MULTILINE, DOTALL, VERBOSE (admite comentarios/espacios en patrones). Los grupos con nombre (?P<name>...) mejoran la legibilidad. Lookahead (?=) y lookbehind (?<=) coinciden sin consumir.
import re
# Compile pattern for reuse (faster when used many times)
email_re = re.compile(r"^[\w.+-]+@([\w-]+\.)+[\w-]+$")
print(email_re.match("[email protected]")) # match object
print(email_re.match("invalid")) # None
# Common flags
re.IGNORECASE # case-insensitive
re.MULTILINE # ^ and $ match line boundaries
re.DOTALL # . matches newline too
re.VERBOSE # allow whitespace & comments in pattern
# Combine flags with |
pattern = re.compile(r"""
^ # start of line
(\w+) # capture word
\s+ # whitespace
(\d+) # capture number
""", re.VERBOSE | re.MULTILINE)
# Named groups (more readable)
m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2024-01")
print(m.group("year")) # 2024
print(m.group("month")) # 01
print(m.groupdict()) # {'year': '2024', 'month': '01'}
# Lookahead/lookbehind
re.findall(r"\d+(?= dollars)", "100 dollars, 200 euros")
# ['100'] (positive lookahead)
re.findall(r"(?<=\$)\d+", "$100 and $200")
# ['100', '200'] (positive lookbehind)Async y Concurrencia
Fundamentos de asyncio
asyncio es el framework de E/S asíncrona de Python. 'async def' define una corrutina; 'await' se suspende hasta que un resultado está listo. asyncio.run() inicia el event loop. asyncio.gather() ejecuta corrutinas concurrentemente. Las corrutinas habilitan E/S de alta concurrencia sin hilos.
import asyncio
# Define a coroutine
async def greet(name, delay):
await asyncio.sleep(delay) # non-blocking sleep
return f"Hello, {name}!"
# Run a coroutine
async def main():
result = await greet("Alice", 1)
print(result)
# Run the event loop
asyncio.run(main())
# Concurrent execution with gather
async def main():
# Run coroutines concurrently
results = await asyncio.gather(
greet("Alice", 2),
greet("Bob", 1),
greet("Carol", 3)
)
print(results) # all complete after 3 seconds (max delay)
asyncio.run(main())
# asyncio.create_task - schedule without awaiting immediately
async def main():
task = asyncio.create_task(greet("Alice", 1))
# do other work here
result = await task # await when needed
print(result)HTTP Asíncrono y Timeouts
asyncio.timeout() (Python 3.11+) cancela operaciones que tardan demasiado. Para HTTP, usa aiohttp (async) en lugar de requests (sync). asyncio.Queue habilita patrones productor-consumidor. Async es ideal para trabajo limitado por E/S (red, disco), no para trabajo limitado por CPU.
import asyncio
# Async timeout
async def fetch_with_timeout(url, timeout=5):
try:
async with asyncio.timeout(timeout):
# simulate async operation
await asyncio.sleep(2)
return f"Data from {url}"
except asyncio.TimeoutError:
return "Request timed out"
# Using aiohttp (third-party) for HTTP
# import aiohttp
#
# async def fetch(url):
# async with aiohttp.ClientSession() as session:
# async with session.get(url) as response:
# return await response.text()
# Producer-consumer pattern
async def producer(queue):
for i in range(5):
await asyncio.sleep(0.1)
await queue.put(f"item-{i}")
await queue.put(None) # sentinel
async def consumer(queue):
while True:
item = await queue.get()
if item is None:
break
print(f"Processed: {item}")
queue.task_done()
async def main():
queue = asyncio.Queue()
await asyncio.gather(producer(queue), consumer(queue))
asyncio.run(main())Threading
Threading es para concurrencia limitada por E/S (red, E/S de archivos). El GIL de Python impide la ejecución paralela real de CPU en hilos. Usa Lock para proteger el estado compartido de condiciones de carrera. Para trabajo limitado por CPU, usa multiprocessing en su lugar. Los hilos comparten memoria; los procesos no.
import threading
import time
# Basic threading
def worker(name, delay):
print(f"Worker {name} starting")
time.sleep(delay)
print(f"Worker {name} done")
# Create and start threads
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 1))
t1.start()
t2.start()
# Wait for threads to complete
t1.join()
t2.join()
print("All done")
# Thread with Lock (for shared state)
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # acquire/release lock
counter += 1
threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Counter: {counter}") # 500000 (correct with lock)Multiprocessing
Multiprocessing evita el GIL para paralelismo real de CPU: cada proceso tiene su propio intérprete de Python. Usa Pool para operaciones map paralelas. concurrent.futures proporciona una API unificada tanto para hilos como para procesos. Protégete siempre con if __name__ == '__main__' en Windows.
from multiprocessing import Process, Pool, Queue
import os
# Basic process
def worker(name):
print(f"Process {name} PID: {os.getpid()}")
if __name__ == "__main__":
p = Process(target=worker, args=("A",))
p.start()
p.join()
# Process pool for parallel work
def square(x):
return x * x
if __name__ == "__main__":
with Pool(4) as pool: # 4 worker processes
results = pool.map(square, range(10))
print(results) # [0, 1, 4, 9, ..., 81]
# Asynchronous map
with Pool(4) as pool:
result = pool.map_async(square, range(10))
print(result.get()) # blocks until done
# concurrent.futures (higher-level API)
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
with ProcessPoolExecutor() as executor:
results = list(executor.map(square, range(10)))
with ThreadPoolExecutor() as executor:
futures = [executor.submit(square, i) for i in range(10)]
results = [f.result() for f in futures]Decoradores
Decorador Básico
Los decoradores envuelven una función para extender o modificar su comportamiento sin cambiar el código fuente original. La sintaxis @ es azúcar sintáctico para asignar el resultado de la llamada al decorador de vuelta al nombre de la función. Usa *args, **kwargs en el wrapper para que funcione con cualquier firma.
# A decorator is a function that takes a function and returns a new function
def uppercase_result(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase_result
def greet(name):
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
# @uppercase_result is sugar for: greet = uppercase_result(greet)functools.wraps (Preservar Metadatos)
Sin @wraps, la función envuelta pierde su __name__, __doc__ y firma originales: las herramientas de depuración y help() muestran 'wrapper' en su lugar. Usa siempre @functools.wraps(func) dentro de los decoradores para preservar los metadatos. Es una buena práctica casi universal.
from functools import wraps
def log_calls(func):
@wraps(func) # copies __name__, __doc__, __module__
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
"""Add two numbers."""
return a + b
print(add.__name__) # 'add' (not 'wrapper')
print(add.__doc__) # 'Add two numbers.'
help(add) # shows original docstringDecorador con Argumentos
Cuando un decorador toma argumentos, necesitas tres niveles de anidamiento: la fábrica (toma args), el decorador (toma la función) y el wrapper (toma los argumentos de llamada). @repeat(3) llama primero a repeat(3), que devuelve el decorador, que luego se aplica a la función.
# A decorator factory: returns the actual decorator
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi(name):
print(f"Hi, {name}!")
say_hi("Alice")
# Hi, Alice! (printed 3 times)Decorador Basado en Clases
Los decoradores de clase usan __init__ para almacenar la función y __call__ para interceptar invocaciones. Son ideales cuando el decorador necesita mantener estado (como un contador de llamadas o caché). La instancia de la clase reemplaza a la función, por lo que llamarla dispara __call__.
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
wraps(func)(self) # preserve metadata
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} called {self.count} times")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # count=1
say_hello() # count=2
say_hello() # count=3
print(say_hello.count) # 3Decoradores Integrados (@property, @staticmethod, @classmethod)
@property convierte un método en un atributo calculado (accedido sin paréntesis). @classmethod recibe la clase como primer argumento: perfecto para constructores alternativos. @staticmethod no recibe primer argumento implícito: solo una función que vive en el espacio de nombres de la clase. Juntos forman la columna vertebral de la POO pitónica.
class Circle:
pi = 3.14159
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return Circle.pi * self._radius ** 2
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@classmethod
def from_diameter(cls, diameter):
return cls(diameter / 2)
@staticmethod
def is_valid_radius(r):
return r >= 0
c = Circle(5)
print(c.area) # 78.54 (no parentheses!)
c.radius = 10 # uses the setter
c2 = Circle.from_diameter(20) # alternative constructorDecoradores Apilados
Al apilar decoradores, se aplican de abajo a arriba (el más cercano a la función se ejecuta primero) pero se ejecutan de arriba a abajo en tiempo de llamada. Así @bold envuelve @italic que envuelve greet. El resultado se anida como capas de cebolla. El orden importa: invertirlos cambia el anidamiento de salida.
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("World"))
# <b><i>Hello, World</i></b>
# Applied bottom-up: italic first, then boldGeneradores e Iteradores
Funciones Generadoras (yield)
Los generadores producen valores de forma perezosa usando yield: pausan la ejecución tras cada yield y se reanudan cuando se llama a next(). Esto los hace eficientes en memoria para secuencias grandes o infinitas, ya que solo existe un valor en memoria a la vez. Una vez agotado, un generador no puede reutilizarse.
# A generator uses 'yield' to produce values lazily, one at a time
def count_up_to(n):
count = 1
while count <= n:
yield count
count += 1
gen = count_up_to(5)
print(next(gen)) # 1
print(next(gen)) # 2
print(list(gen)) # [3, 4, 5] (exhausts the rest)
# Generators are memory-efficient: they don't build the whole list
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Expresiones Generadoras
Las expresiones generadoras son el equivalente perezoso de las list comprehensions: usa paréntesis en lugar de corchetes. Usan memoria constante independientemente del tamaño, lo que las hace ideales para sum(), max(), any(), o para alimentar otros iteradores. Prefiérelas sobre list comprehensions cuando no necesites acceso aleatorio.
# Like list comprehensions, but lazy (uses parentheses)
squares_list = [x ** 2 for x in range(10)] # builds full list
squares_gen = (x ** 2 for x in range(10)) # lazy generator
print(squares_gen) # <generator object>
print(next(squares_gen)) # 0
print(next(squares_gen)) # 1
# Memory comparison
import sys
print(sys.getsizeof([x for x in range(10000)])) # ~87616 bytes
print(sys.getsizeof((x for x in range(10000)))) # ~200 bytes (constant!)
# Use in sum(), list(), any() etc.
total = sum(x ** 2 for x in range(100)) # no extra list created
print(total) # 328350Protocolo Iterador (__iter__, __next__)
El protocolo iterador requiere __iter__ (devuelve un iterador) y __next__ (devuelve el siguiente valor o lanza StopIteration). Los iterables pueden recorrerse en bucle; los iteradores producen valores uno a uno. Para iterables reutilizables, separa el iterable (devuelve un iterador nuevo) del iterador (mantiene el estado).
class Range2:
"""A custom iterator that yields even numbers."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self # the object is its own iterator
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 2
return value
r = Range2(0, 10)
for num in r:
print(num) # 0, 2, 4, 6, 8
# An iterable returns a fresh iterator each time __iter__ is called.
# An iterator returns itself and raises StopIteration when exhausted.send(), throw(), close()
Los métodos avanzados de generador habilitan comunicación bidireccional: send() pasa un valor al generador (se convierte en el resultado de yield), throw() inyecta una excepción en el punto de yield, y close() termina el generador. Debes 'cebar' el generador con next() antes de enviar. Estos potencian corrutinas y frameworks async.
def echo():
while True:
received = yield # yield without a value, receives via send()
print(f"Echo: {received}")
gen = echo()
next(gen) # prime the generator (advance to first yield)
gen.send("hello") # Echo: hello
gen.send("world") # Echo: world
# throw() injects an exception at the yield point
def safe_gen():
try:
while True:
yield
except ValueError:
print("Caught ValueError inside generator")
g = safe_gen()
next(g)
g.throw(ValueError, "boom") # Caught ValueError inside generator
# close() stops the generator (raises GeneratorExit)
gen.close()Pipelines de Generadores
Los pipelines de generadores encadenan productores perezosos para que los datos fluyan por etapas un elemento cada vez: cada elemento se procesa completamente antes de leer el siguiente. Esto evita construir listas intermedias y es la base del procesamiento de datos en streaming. Los pipes de Unix funcionan igual conceptualmente.
# Chain generators to build data-processing pipelines
def numbers():
for i in range(1, 11):
yield i
def squared(seq):
for n in seq:
yield n ** 2
def evens(seq):
for n in seq:
if n % 2 == 0:
yield n
# Each stage processes one item at a time — no intermediate lists
pipeline = evens(squared(numbers()))
print(list(pipeline)) # [4, 16, 36, 64, 100]
# Equivalent with generator expressions:
result = (n for n in (x ** 2 for x in range(1, 11)) if n % 2 == 0)
print(list(result)) # [4, 16, 36, 64, 100]yield from (Delegación)
yield from delega todos los yields (y send/throw/close) a un sub-iterador, aplanando estructuras anidadas y componiendo corrutinas. Es especialmente potente para generadores recursivos: el caso de uso clásico es aplanar listas anidadas arbitrariamente. En código async, 'await' se construye sobre el mismo concepto.
# yield from delegates to a sub-iterator (Python 3.3+)
def flatten(nested):
for item in nested:
if isinstance(item, (list, tuple)):
yield from flatten(item) # recursive delegation
else:
yield item
data = [1, [2, 3, [4, 5]], 6, [7, [8, [9]]]]
print(list(flatten(data)))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# yield from also forwards send()/throw() to the sub-generator,
# making it essential for coroutine composition.Gestores de Contexto
Fundamentos de la Sentencia with
La sentencia with garantiza que los recursos se liberen (archivos cerrados, locks liberados, conexiones devueltas) incluso cuando ocurren excepciones. Llama a __enter__ al inicio y __exit__ al final. Prefiere siempre 'with' sobre try/finally manual para gestión de recursos: es más seguro y legible.
# 'with' guarantees cleanup even if an exception occurs
with open("data.txt", "r") as f:
content = f.read()
# f.close() is called automatically here, even if read() raised
# Without 'with' you must manually close:
f = open("data.txt", "r")
try:
content = f.read()
finally:
f.close() # easy to forget!
# Common built-in context managers:
with open("out.txt", "w") as f, open("in.txt") as g:
f.write(g.read()) # both files close automaticallyGestor de Contexto Personalizado (Clase)
Un gestor de contexto basado en clase implementa __enter__ (setup, devuelve el objeto de contexto) y __exit__(exc_type, exc_val, exc_tb) (limpieza). Los argumentos de __exit__ reciben información de excepción si ocurrió una; devolver True la suprime. Este patrón es ideal para setup/teardown complejo como transacciones de base de datos.
class Timer:
def __init__(self, label="Timer"):
self.label = label
def __enter__(self):
import time
self.start = time.perf_counter()
return self # value bound to 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.perf_counter() - self.start
print(f"{self.label}: {elapsed:.4f}s")
# Return False (or None) to propagate exceptions
# Return True to suppress the exception
return False
with Timer("Processing"):
total = sum(i ** 2 for i in range(1_000_000))
# Processing: 0.1234scontextlib.contextmanager
contextlib.contextmanager convierte una función generadora en un gestor de contexto: el código antes de yield es __enter__, el código después de yield (en finally) es __exit__. Es más conciso que una clase para casos simples. Haz yield de un valor para proporcionarlo a la variable 'as'. Usa try/finally para garantizar la limpieza.
from contextlib import contextmanager
import time
@contextmanager
def timer(label="Timer"):
start = time.perf_counter()
try:
yield # code inside the 'with' block runs here
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s")
with timer("My task"):
sum(i ** 2 for i in range(1_000_000))
# My task: 0.1234s
# You can also yield a value to bind with 'as'
@contextmanager
def open_db(path):
db = connect(path)
try:
yield db
finally:
db.close()
with open_db("app.db") as db:
db.query("SELECT 1")Múltiples Gestores de Contexto
Python 3.10+ permite sentencias 'with' multilínea entre paréntesis para una sintaxis más limpia. Para números dinámicos de gestores de contexto, contextlib.ExitStack los gestiona como grupo y los desenrolla en orden inverso. ExitStack es esencial cuando el número de recursos no se conoce hasta el tiempo de ejecución.
# Python 3.10+ supports parenthesized context managers
with (
open("input.txt") as fin,
open("output.txt", "w") as fout,
):
fout.write(fin.read())
# Pre-3.10: nest them or use contextlib.ExitStack
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [stack.enter_context(open(f)) for f in files]
for h in handles:
print(h.read())Utilidades de contextlib (suppress, redirect)
contextlib.suppress reemplaza try/except/pass para excepciones esperadas: mucho más legible. redirect_stdout/redirect_stderr capturan la salida que de otro modo iría a la consola, útil para pruebas o logging. Estas utilidades evitan boilerplate y hacen explícita la intención.
from contextlib import suppress, redirect_stdout, redirect_stderr
import io, warnings
# suppress: ignore specific exceptions (cleaner than try/except/pass)
with suppress(FileNotFoundError):
os.remove("temp.txt") # no error if file doesn't exist
# redirect_stdout: capture print output
buffer = io.StringIO()
with redirect_stdout(buffer):
print("This goes to the buffer, not console")
captured = buffer.getvalue()
# redirect_stderr: capture error/warning output
err_buf = io.StringIO()
with redirect_stderr(err_buf):
warnings.warn("a warning")
print(err_buf.getvalue()) # the warning text
# Also: contextlib.chdir (3.11+) to temporarily change directory
# from contextlib import chdir
# with chdir("/tmp"): ...Gestores de Contexto Asíncronos
Los gestores de contexto asíncronos usan __aenter__/__aexit__ (observa el prefijo 'a') y la sentencia 'async with'. Son esenciales para gestionar recursos async como conexiones a bases de datos o sesiones HTTP (por ejemplo, aiohttp.ClientSession). La limpieza se ejecuta aunque ocurra un await o una excepción dentro del bloque.
import asyncio
class AsyncDB:
async def __aenter__(self):
print("connecting...")
await asyncio.sleep(0.1) # simulate async connect
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("closing...")
await asyncio.sleep(0.1) # simulate async close
return False
async def query(self, sql):
return f"result of: {sql}"
async def main():
async with AsyncDB() as db:
result = await db.query("SELECT 1")
print(result)
asyncio.run(main())
# connecting...
# result of: SELECT 1
# closing...Anotaciones de Tipo
Anotaciones Básicas de Variables y Funciones
Las anotaciones de tipo documentan los tipos esperados pero NO se imponen en tiempo de ejecución: Python sigue siendo de tipado dinámico. Usa un comprobador estático como mypy o pyright para captar errores de tipo antes de ejecutar. Los genéricos integrados (list[str], dict[str, int]) requieren Python 3.9+; versiones anteriores necesitan typing.List, typing.Dict.
# Variable annotations (Python 3.6+)
name: str = "Alice"
age: int = 30
scores: list[float] = [95.5, 88.0, 92.3]
config: dict[str, int] = {"timeout": 30}
# Function annotations
def greet(name: str, excited: bool = False) -> str:
punctuation = "!" if excited else "."
return f"Hello, {name}{punctuation}"
print(greet("World", excited=True)) # Hello, World!
# Annotations are optional and NOT enforced at runtime
def add(a: int, b: int) -> int:
return a + b
add("2", "3") # runs fine, returns "23" (no error!)Módulo typing (List, Dict, Tuple, Optional)
El módulo typing proporciona alias genéricos para Python antiguo. Desde 3.9, puedes usar tipos integrados directamente (list[str] en lugar de List[str]). Optional[X] es abreviatura de Union[X, None]: úsalo para señalar que una función puede devolver None, forzando a los llamadores a manejar el caso None.
from typing import List, Dict, Tuple, Set, FrozenSet
# Pre-3.9 style (still works, needed for older Python)
names: List[str] = ["Alice", "Bob"]
scores: Dict[str, int] = {"Alice": 95}
point: Tuple[int, int] = (10, 20)
mixed: Tuple[str, int, float] = ("a", 1, 2.0)
variadic: Tuple[int, ...] = (1, 2, 3) # variable-length
# Python 3.9+ built-in generics (preferred)
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95}
point: tuple[int, int] = (10, 20)
# Optional means "could be None"
from typing import Optional
def find_user(uid: int) -> Optional[str]:
if uid == 1:
return "Alice"
return None # could also just 'return'Tipos Union y Literal
Union[X, Y] (o X | Y en 3.10+) significa que un valor puede ser de cualquiera de los tipos. Literal restringe un valor a constantes específicas: genial para enums de cadena sin la sobrecarga de enum, y para dispatch de funciones sobrecargadas. mypy usa Literal para acotar tipos y captar argumentos inválidos en tiempo de comprobación.
from typing import Union, Literal, overload
# Union: value can be one of several types
def process(data: Union[str, bytes]) -> str:
if isinstance(data, bytes):
return data.decode("utf-8")
return data
# Python 3.10+ union syntax with | (preferred)
def process2(data: str | bytes) -> str:
if isinstance(data, bytes):
return data.decode("utf-8")
return data
# Literal: restrict to specific constant values
def set_mode(mode: Literal["r", "w", "a"]) -> None:
print(f"Mode set to {mode}")
set_mode("r") # OK
# set_mode("x") # mypy error: not a valid literal
# Literal for boolean-like flags
Direction = Literal["up", "down", "left", "right"]TypeVar y Genéricos
TypeVar crea variables de tipo genéricas para que funciones y clases puedan preservar relaciones de tipo (por ejemplo, 'devuelve el mismo tipo que la entrada'). Usa bound= para restringir a un subtipo, o especifica restricciones como TypeVar('T', int, float). Las clases genéricas usan Generic[T] como base para convertirse en contenedores parametrizados.
from typing import TypeVar, Generic, List
T = TypeVar("T") # a generic type variable
def first(items: List[T]) -> T:
return items[0]
# Type inference: T is bound to the argument's type
x: int = first([1, 2, 3]) # T = int
y: str = first(["a", "b", "c"]) # T = str
# Bounded TypeVar: T must be a subtype of Number
from typing import TypeVar
from numbers import Number
N = TypeVar("N", bound=Number)
def sum_all(values: list[N]) -> N:
total = values[0]
for v in values[1:]:
total = total + v
return total
# Generic class
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
s: Stack[int] = Stack()
s.push(1)
# s.push("x") # mypy errorCallable, Alias de Tipo y Protocols
Callable[[int, str], bool] describe una función que toma int y str y devuelve bool. Los alias de tipo dan nombres descriptivos a tipos complejos. Protocol habilita el tipado estructural (duck typing): cualquier objeto con los métodos correctos satisface el protocolo, sin necesidad de herencia. Es la respuesta de Python a las interfaces.
from typing import Callable, Protocol, TypeAlias
# Callable signature: Callable[[ArgTypes], ReturnType]
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
apply(lambda x, y: x + y, 3, 4) # 7
# Type aliases (3.12+ uses 'type' statement; older uses assignment)
type Vector = list[float] # 3.12+
Vector2: TypeAlias = list[float] # 3.10+
def magnitude(v: Vector) -> float:
return sum(x ** 2 for x in v) ** 0.5
# Protocol: structural typing (duck typing with static checks)
class Drawable(Protocol):
def draw(self) -> None: ...
def render(obj: Drawable) -> None:
obj.draw() # any object with a draw() method works
class Circle:
def draw(self) -> None:
print("drawing circle")
render(Circle()) # OK — Circle has draw()Comprobación de Tipos con mypy
mypy es el comprobador de tipos estático más popular para Python: analiza las anotaciones de tipo sin ejecutar el código. Captura bugs de manejo de None, tipos de argumento incorrectos y returns faltantes. Empieza con tipado gradual: añade anotaciones al código nuevo y ejecuta mypy en CI. Usa --strict para nuevos proyectos y forzar anotaciones completas.
# Save as example.py, then run: mypy example.py
from typing import Optional
def divide(a: float, b: float) -> Optional[float]:
if b == 0:
return None
return a / b
result = divide(10, 0)
# Without checking, this would crash at runtime:
# print(result + 1) # TypeError: NoneType + int
# mypy catches it:
# error: Unsupported operand types for + ("None" and "int")
# fix: check for None first
if result is not None:
print(result + 1)
# Run strict mode for maximum safety:
# mypy --strict example.py
# Common strict flags: --disallow-untyped-defs, --no-implicit-optionalData Classes
@dataclass Básico
@dataclass genera automáticamente __init__, __repr__ y __eq__ basándose en campos anotados: elimina el boilerplate para clases contenedoras de datos. Es ideal para objetos valor, configs, DTOs y records. Disponible desde Python 3.7. Los campos deben tener anotaciones de tipo; la anotación define el campo.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1) # Point(x=1.0, y=2.0) — auto __repr__
print(p1 == p2) # True — auto __eq__ (compares fields)
print(p1.x) # 1.0
# Without @dataclass you'd write all this boilerplate:
# class Point:
# def __init__(self, x, y): self.x = x; self.y = y
# def __repr__(self): ...
# def __eq__(self, other): ...Valores por Defecto y default_factory
Los defaults mutables (listas, dicts, sets) deben usar field(default_factory=list): usar [] directamente compartiría una lista entre todas las instancias, un bug clásico. default_factory se llama una vez por instancia para crear un objeto nuevo. Los defaults inmutables simples (int, str, bool, None) pueden asignarse directamente.
from dataclasses import dataclass, field
@dataclass
class Student:
name: str
grade: str = "A" # simple default
tags: list[str] = field(default_factory=list) # mutable default!
scores: dict[str, int] = field(default_factory=dict)
s = Student("Alice")
print(s) # Student(name='Alice', grade='A', tags=[], scores={})
s.tags.append("honors")
s2 = Student("Bob")
print(s2.tags) # [] — each instance gets its own list
# NEVER use [] or {} as a direct default — all instances would share
# the same mutable object (classic Python pitfall).frozen, eq y order
frozen=True hace inmutable un dataclass: los campos no pueden reasignarse, y la instancia se vuelve hashable (usable como claves de dict o miembros de set). order=True añade métodos de comparación para ordenación. Combina frozen=True con order=True para tipos valor inmutables y ordenables como coordenadas, colores o versiones.
from dataclasses import dataclass
# frozen=True makes instances immutable (hashable, usable as dict keys)
@dataclass(frozen=True)
class Color:
r: int
g: int
b: int
c = Color(255, 0, 0)
# c.r = 128 # FrozenInstanceError!
print(hash(c)) # works — frozen dataclasses are hashable
# order=True generates __lt__, __le__, __gt__, __ge__ for sorting
@dataclass(order=True)
class Priority:
level: int
tasks = [Priority(3), Priority(1), Priority(2)]
tasks.sort()
print(tasks) # [Priority(level=1), Priority(level=2), Priority(level=3)]
# Common combo: frozen + order for immutable comparable values
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int__post_init__ y personalización de field
__post_init__ se ejecuta automáticamente después del __init__ generado: úsalo para calcular campos derivados, validar valores o hacer setup. field(init=False) crea un campo que no está en el constructor (bueno para valores calculados/en caché). field(repr=False, compare=False) oculta campos de repr y comprobaciones de igualdad.
from dataclasses import dataclass, field
@dataclass
class User:
email: str
_email_normalized: str = field(init=False, repr=False)
id: int = field(default=0)
def __post_init__(self):
# runs after __init__; compute derived fields here
self._email_normalized = self.email.strip().lower()
u = User(" [email protected] ")
print(u.email) # ' [email protected] '
print(u._email_normalized) # '[email protected]'
# field(init=False) excludes a field from __init__
# field(repr=False) hides it from the repr
# field(compare=False) excludes from __eq__/__hash__
# field(metadata={...}) attaches custom metadataHerencia y slots
Los dataclasses admiten herencia: los campos hijo se añaden después de los padre, y puedes sobrescribir los defaults padre. Nota: un campo con default en el padre no puede ir seguido de un campo sin default en el hijo. slots=True (3.10+) impide añadir atributos arbitrarios y reduce significativamente la memoria por instancia: ideal para millones de objetos pequeños.
from dataclasses import dataclass
@dataclass
class Animal:
name: str
sound: str = "..."
@dataclass
class Dog(Animal):
breed: str = "unknown"
sound: str = "Woof" # override parent default
d = Dog("Rex", breed="Labrador")
print(d) # Dog(name='Rex', sound='Woof', breed='Labrador')
# Python 3.10+: slots=True saves memory (no __dict__)
@dataclass(slots=True)
class Pixel:
r: int
g: int
b: int
p = Pixel(0, 128, 255)
# p.new_field = 1 # AttributeError — slots prevent arbitrary attrs
# Saves ~40-50% memory vs regular dataclass for many instancesCollections e Itertools
namedtuple
namedtuple crea subclases de tupla con campos con nombre: tan eficientes en memoria como las tuplas pero mucho más legibles. Son inmutables, así que usa _replace() para crear copias modificadas. Prefiere typing.NamedTuple para código nuevo ya que admite anotaciones de tipo y valores por defecto. Genial para devolver múltiples valores de funciones.
from collections import namedtuple
# Lightweight immutable class with named fields
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4 — access by name
print(p[0], p[1]) # 3 4 — also by index
print(p._asdict()) # {'x': 3, 'y': 4}
# More memory-efficient than a full class
# Use _replace to create a modified copy (immutable!)
p2 = p._replace(x=10)
print(p2) # Point(x=10, y=4)
# Python 3.6+ typing.NamedTuple for type hints:
from typing import NamedTuple
class Point3D(NamedTuple):
x: float
y: float
z: float = 0.0Counter
Counter es una subclase de dict para contar objetos hashable: perfecto para análisis de frecuencia, histogramas y votaciones. most_common(n) devuelve los n elementos principales. Las claves ausentes devuelven 0 en lugar de lanzar KeyError. Counter admite +, -, &, | para aritmética de conjuntos sobre conteos.
from collections import Counter
# Count hashable items
words = "the cat sat on the mat the cat".split()
c = Counter(words)
print(c) # Counter({'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1})
# Most common elements
print(c.most_common(2)) # [('the', 3), ('cat', 2)]
# Arithmetic on counters
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2) # Counter({'a': 4, 'b': 3})
print(c1 - c2) # Counter({'a': 2}) (drops zero/negatives)
# Missing keys return 0 (not KeyError)
print(c["dog"]) # 0
# Update and elements
c.update(["cat", "cat"])
print(sorted(c.elements())) # ['cat','cat','cat','cat','mat','on','sat','the','the','the']defaultdict
defaultdict crea automáticamente las claves ausentes con un valor por defecto de una función factory: list para agrupar, int para contar, set para desduplicar. Esto elimina el boilerplate de 'if key not in dict'. La factory se llama solo cuando falta una clave, no en cada acceso.
from collections import defaultdict
# Group items by key without checking if key exists
words = ["apple", "banana", "avocado", "blueberry", "cherry"]
by_first = defaultdict(list)
for w in words:
by_first[w[0]].append(w)
print(dict(by_first))
# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
# Counting with int (default 0)
counts = defaultdict(int)
for w in words:
counts[w[0]] += 1
print(dict(counts)) # {'a': 2, 'b': 2, 'c': 1}
# Nested defaultdicts
tree = defaultdict(lambda: defaultdict(list))
tree["2024"]["Jan"].append("event1")
# vs regular dict: avoids the key-check boilerplate
# d = {}
# for w in words:
# if w[0] not in d:
# d[w[0]] = []
# d[w[0]].append(w)OrderedDict y deque
deque proporciona append/pop en O(1) en ambos extremos: úsalo para colas, BFS y ventanas deslizantes en lugar de listas (list.pop(0) es O(n)). Con maxlen, deque descarta automáticamente elementos antiguos, perfecto para búferes acotados. OrderedDict es menos necesario desde 3.7 (los dicts son ordenados), pero su move_to_end y popitem siguen siendo únicos para cachés LRU.
from collections import OrderedDict, deque
# deque: double-ended queue, O(1) append/pop at both ends
dq = deque([1, 2, 3], maxlen=5)
dq.appendleft(0) # deque([0, 1, 2, 3])
dq.append(4) # deque([0, 1, 2, 3, 4])
dq.append(5) # deque([1, 2, 3, 4, 5]) — oldest dropped (maxlen!)
print(dq.popleft()) # 1
print(dq) # deque([2, 3, 4, 5])
# deque is ideal for queues, BFS, sliding windows
from collections import deque
queue = deque(["task1", "task2"])
queue.append("task3")
next_task = queue.popleft() # FIFO — O(1) vs list.pop(0) which is O(n)
# OrderedDict: remembers insertion order (regular dicts do too in 3.7+,
# but OrderedDict has move_to_end and equality is order-sensitive)
od = OrderedDict([("a", 1), ("b", 2)])
od.move_to_end("a") # move to last
print(list(od)) # ['b', 'a']
od.popitem(last=False) # pop first item (FIFO)itertools: chain, product, combinations, permutations
itertools proporciona herramientas rápidas y eficientes en memoria para combinatoria. chain aplana iterables de forma perezosa. product da productos cartesianos (reemplaza bucles for anidados). combinations/permutations generan selecciones sin construir la lista completa: esencial para entradas grandes o infinitas. Todos devuelven iteradores, así que envuélvelos en list() para verlos.
from itertools import chain, product, combinations, permutations
# chain: flatten multiple iterables
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]])) # [1, 2, 3, 4]
# product: Cartesian product (nested loops)
list(product([1, 2], ["a", "b"]))
# [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product("AB", repeat=2)) # [('A','A'),('A','B'),('B','A'),('B','B')]
# combinations: unordered selections (no repeats)
list(combinations("ABC", 2)) # [('A','B'),('A','C'),('B','C')]
list(combinations("AAA", 2)) # [('A','A'),('A','A'),('A','A')]
# permutations: ordered arrangements
list(permutations("ABC", 2)) # [('A','B'),('A','C'),('B','A'),('B','C'),('C','A'),('C','B')]
# combinations_with_replacement: allow picking same element
from itertools import combinations_with_replacement
list(combinations_with_replacement("AB", 2)) # [('A','A'),('A','B'),('B','B')]itertools: groupby, accumulate, starmap
groupby agrupa elementos consecutivos que comparten una clave: ordena por la clave primero o obtendrás múltiples grupos para la misma clave. accumulate produce totales/productos acumulados. islice, takewhile y dropwhile son alternativas perezosas a slicing y filtrado que funcionan en cualquier iterador, incluidos los infinitos.
from itertools import groupby, accumulate, starmap, islice, takewhile, dropwhile
# groupby: group consecutive items by a key (sort first!)
data = [("A", 1), ("A", 2), ("B", 3), ("B", 4), ("A", 5)]
data.sort(key=lambda x: x[0]) # MUST sort by key first
for key, group in groupby(data, key=lambda x: x[0]):
print(key, list(group))
# A [('A',1),('A',2),('A',5)]
# B [('B',3),('B',4)]
# accumulate: running aggregate (sum by default)
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10]
import operator
list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24]
# starmap: unpack args from tuples before calling
list(starmap(pow, [(2, 3), (3, 2), (10, 3)])) # [8, 9, 1000]
# islice: slice an iterator (doesn't support negative indices)
list(islice(range(100), 5, 10)) # [5, 6, 7, 8, 9]
# takewhile / dropwhile: filter by predicate
list(takewhile(lambda x: x < 5, [1, 4, 6, 3, 8])) # [1, 4]
list(dropwhile(lambda x: x < 5, [1, 4, 6, 3, 8])) # [6, 3, 8]functools: lru_cache, partial, reduce
lru_cache memoriza resultados: aceleraciones drásticas para funciones puras recursivas o costosas; cache_info() muestra estadísticas de aciertos/fallos. partial pre-rellena argumentos para crear callables especializados. reduce aplica una función acumulativamente (aunque sum(), any(), all() suelen reemplazarlo). cached_property calcula una vez y luego cachea en la instancia.
from functools import lru_cache, partial, reduce
import operator
# lru_cache: memoize function results (Least Recently Used)
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(100)) # instant (without cache: impossibly slow)
print(fib.cache_info()) # CacheInfo(hits=98, misses=101, ...)
# partial: fix some arguments, create a new callable
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(3)) # 27
# reduce: cumulatively apply a function, reducing to one value
product = reduce(operator.mul, [1, 2, 3, 4]) # 24
# Equivalent: ((1*2)*3)*4
# Python 3.8+: cached_property for lazy computed attributes
from functools import cached_property
class Data:
@cached_property
def expensive(self):
print("computing...")
return [i ** 2 for i in range(1000000)]Procesamiento de JSON y CSV
json.dumps y json.loads
json.dumps() (dump string) serializa un objeto Python a una cadena JSON; json.loads() (load string) analiza JSON de vuelta. Usa indent para legibilidad, ensure_ascii=False para mantener los caracteres Unicode legibles, y sort_keys para salida determinista. Las claves JSON deben ser cadenas: las claves int se convierten en cadenas.
import json
# Serialize Python object to JSON string
data = {"name": "Alice", "age": 30, "scores": [95, 88, 92]}
json_str = json.dumps(data)
print(json_str) # {"name": "Alice", "age": 30, "scores": [95, 88, 92]}
# Pretty-print with indent
print(json.dumps(data, indent=2))
# {
# "name": "Alice",
# "age": 30,
# "scores": [95, 88, 92]
# }
# Parse JSON string to Python object
parsed = json.loads(json_str)
print(parsed["name"]) # Alice
print(type(parsed["scores"])) # <class 'list'>
# Sort keys, handle non-ASCII
print(json.dumps({"name": "Zoë"}, ensure_ascii=False, sort_keys=True))Leer y Escribir Archivos JSON
json.dump() escribe directamente a un objeto archivo; json.load() lee de uno. Especifica siempre encoding='utf-8' para portabilidad. Recuerda el mapeo de tipos: los objetos JSON se convierten en dicts, los arrays en listas, y los números en int o float. Los datetimes, sets y objetos personalizados NO son serializables a JSON por defecto.
import json
data = {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}
# Write to file
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# Read from file
with open("data.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["users"][0]["name"]) # Alice
# Type conversions to remember:
# JSON object <-> Python dict
# JSON array <-> Python list
# JSON string <-> Python str
# JSON number <-> Python int/float
# JSON true/false <-> Python True/False
# JSON null <-> Python NoneCodificación JSON Personalizada (datetime, objetos personalizados)
El módulo json no puede serializar datetime, set o clases personalizadas por defecto. Proporciona una función default (llamada para objetos no serializables) o una subclase de JSONEncoder. Para round-tripping, empareja un codificador personalizado con un object_hook en loads() para reconstruir los tipos originales. Así es como los ORMs serializan objetos modelo.
import json
from datetime import datetime
# Default behavior: TypeError on non-serializable types
# json.dumps({"now": datetime.now()}) # TypeError!
# Solution 1: default function for unknown types
def default_encoder(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, set):
return sorted(obj)
raise TypeError(f"Cannot serialize {type(obj)}")
data = {"now": datetime.now(), "tags": {"a", "b"}}
print(json.dumps(data, default=default_encoder))
# Solution 2: custom JSONEncoder subclass
class MyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return {"__datetime__": obj.isoformat()}
return super().default(obj)
print(json.dumps(data, cls=MyEncoder))
# Decoding with object_hook
def decoder(dct):
if "__datetime__" in dct:
return datetime.fromisoformat(dct["__datetime__"])
return dct
json.loads(json.dumps(data, cls=MyEncoder), object_hook=decoder)Leer Archivos CSV
Abre siempre archivos CSV con newline='' para evitar problemas de filas en blanco en Windows. csv.reader devuelve listas; csv.DictReader devuelve dicts con clave la fila de cabecera. El módulo csv maneja comillado, comas incrustadas y saltos de línea correctamente: nunca dividas líneas CSV manualmente con line.split(','). Usa Sniffer para autodetectar delimitadores.
import csv
# Basic reader: each row is a list of strings
with open("data.csv", newline="", encoding="utf-8") as f:
reader = csv.reader(f)
for row in reader:
print(row) # ['name', 'age', 'city']
# DictReader: each row is a dict keyed by header
with open("data.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["age"]) # access by column name
# Handle different delimiters and quoting
with open("data.tsv", newline="") as f:
reader = csv.reader(f, delimiter="\t", quotechar='"')
for row in reader:
print(row)
# Sniffer to auto-detect format
with open("unknown.csv", newline="") as f:
sample = f.read(1024)
dialect = csv.Sniffer().sniff(sample)
f.seek(0)
reader = csv.reader(f, dialect)Escribir Archivos CSV
csv.writer escribe listas; csv.DictWriter escribe dicts con un conjunto fijo de fieldnames. Usa siempre newline='' al abrir el archivo. El parámetro quoting controla cuándo se citan los campos: QUOTE_MINIMAL (por defecto) solo cita cuando es necesario, QUOTE_ALL cita todo, útil para parsers estrictos.
import csv
rows = [
["name", "age", "city"],
["Alice", 30, "NYC"],
["Bob", 25, "LA"],
]
# Basic writer
with open("out.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(rows) # write multiple rows
# DictWriter: write from dicts
with open("out.csv", "w", newline="") as f:
fieldnames = ["name", "age", "city"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerow({"name": "Alice", "age": 30, "city": "NYC"})
writer.writerow({"name": "Bob", "age": 25, "city": "LA"})
# Control quoting: QUOTE_MINIMAL (default), QUOTE_ALL, QUOTE_NONNUMERIC
writer = csv.writer(f, quoting=csv.QUOTE_ALL)
# QUOTE_ALL wraps every field in quotes: "Alice","30","NYC"JSON Lines (NDJSON) y Streaming
JSON Lines (NDJSON) pone un objeto JSON por línea: ideal para logs, flujos de eventos y archivos solo de añadir porque puedes procesar cada línea independientemente. Para documentos JSON únicos enormes, usa la librería ijson para parseo en streaming sin cargar todo el archivo en memoria. NDJSON es el estándar para muchos pipelines de datos.
import json
# JSON Lines: one JSON object per line (great for logs, big data)
records = [{"id": 1, "msg": "first"}, {"id": 2, "msg": "second"}]
# Write NDJSON
with open("logs.jsonl", "w") as f:
for rec in records:
f.write(json.dumps(rec) + "\n")
# Read NDJSON line by line (memory-efficient for huge files)
with open("logs.jsonl", "r") as f:
for line in f:
rec = json.loads(line)
print(rec["id"], rec["msg"])
# Stream large JSON arrays without loading everything into memory
# Use ijson library for streaming parsing of huge JSON files:
# import ijson
# with open("huge.json", "rb") as f:
# for item in ijson.items(f, "items.item"):
# process(item) # one item at a timeLogging y Testing
Fundamentos de logging
El módulo logging es la forma estándar de emitir salida de diagnóstico: mucho mejor que print() porque controlas niveles, formatos y destinos. Usa logging.getLogger(__name__) por módulo para poder ajustar la verbosidad por módulo. logging.exception() incluye automáticamente el traceback. Configura basicConfig una vez al inicio.
import logging
# Basic configuration (call once at program start)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Log levels (severity ascending)
logging.debug("Detailed debug info") # DEBUG (10)
logging.info("General information") # INFO (20)
logging.warning("Something unexpected") # WARNING (30)
logging.error("A real error occurred") # ERROR (40)
logging.critical("System is down") # CRITICAL (50)
# Logging exceptions with traceback
try:
1 / 0
except ZeroDivisionError:
logging.exception("Division failed") # includes full traceback
# Get a named logger (best practice per module)
logger = logging.getLogger(__name__)
logger.info("Module-specific log")Logging a Archivo y Múltiples Handlers
Los Handlers enrutan los registros de log a destinos: consola, archivos, red, email. RotatingFileHandler limita el tamaño del archivo y mantiene copias de seguridad, evitando crecimiento ilimitado de logs. Cada handler puede tener su propio nivel y formato (por ejemplo, logs detallados a archivo, logs concisos a consola). TimedRotatingFileHandler rota por tiempo en lugar de por tamaño.
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# Console handler (INFO and above)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
# Rotating file handler (DEBUG and above, max 5MB x 3 backups)
file_handler = RotatingFileHandler(
"app.log", maxBytes=5_000_000, backupCount=3, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
)
logger.addHandler(console)
logger.addHandler(file_handler)
logger.debug("debug to file only")
logger.info("info to both console and file")
logger.error("error everywhere")Fundamentos de unittest
unittest es el framework de pruebas integrado de Python (estilo xUnit). Las pruebas viven en clases que heredan de TestCase. setUp/tearDown se ejecutan antes/después de cada prueba para aislamiento. Aserciones comunes: assertEqual, assertTrue, assertRaises, assertIn. Ejecuta con python -m unittest para autodescubrimiento de archivos test_*.py.
import unittest
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestMath(unittest.TestCase):
def setUp(self):
# runs before each test method
self.data = [1, 2, 3]
def tearDown(self):
# runs after each test method
pass
def test_add(self):
self.assertEqual(add(1, 2), 3)
self.assertEqual(add(-1, 1), 0)
def test_add_types(self):
self.assertEqual(add("a", "b"), "ab")
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(1, 0)
def test_membership(self):
self.assertIn(2, self.data)
self.assertTrue(3 in self.data)
if __name__ == "__main__":
unittest.main()
# Run: python -m unittest test_file.py -vFundamentos de pytest
pytest es la herramienta de testing de Python más popular: simples sentencias assert dan informes de fallo ricos, sin clases boilerplate. pytest.raises comprueba excepciones con coincidencia opcional de regex. pytest.approx maneja la imprecisión de comparación de floats. Instala con pip install pytest y ejecuta con pytest -v para salida verbosa.
# test_math.py — pytest is simpler and more powerful than unittest
# Install: pip install pytest
# Run: pytest -v
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# Plain functions, no classes required
def test_add():
assert add(1, 2) == 3
assert add(-1, 1) == 0
def test_add_strings():
assert add("hello", " world") == "hello world"
# Testing exceptions with pytest.raises
import pytest
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(1, 0)
# Approximate float comparison
def test_float():
assert 0.1 + 0.2 == pytest.approx(0.3)Fixtures de pytest
Las fixtures son la inyección de dependencias de pytest: proporcionan datos de setup, objetos mock o recursos a las pruebas vía nombres de parámetro. Las fixtures basadas en yield manejan tanto el setup (antes de yield) como el teardown (después de yield). Los scopes controlan la reutilización: 'session' crea la fixture una vez para toda la ejecución, 'module' una por archivo, 'function' (por defecto) una por prueba.
import pytest
# A fixture provides setup data/resources to tests
@pytest.fixture
def sample_list():
return [1, 2, 3, 4, 5]
# Use fixtures by passing their name as a parameter
def test_length(sample_list):
assert len(sample_list) == 5
def test_sum(sample_list):
assert sum(sample_list) == 15
# Fixture with setup AND teardown (yield)
@pytest.fixture
def db_connection():
print("\n[setup] connecting to DB")
conn = {"connected": True}
yield conn # test runs here; value passed to test
print("\n[teardown] closing DB")
conn["connected"] = False
def test_db(db_connection):
assert db_connection["connected"] is True
# Fixture scopes: function (default), class, module, session
@pytest.fixture(scope="session")
def expensive_resource():
return load_large_dataset() # created once per test sessionparametrize y mocking en pytest
parametrize ejecuta una sola función de prueba con múltiples conjuntos de entrada: elimina el código de prueba copia-pega y da salida clara por caso. unittest.mock.patch reemplaza funciones/objetos con mocks para testing aislado. assert_called_once_with verifica que el mock se usó correctamente. @pytest.mark.skip y xfail manejan pruebas incompletas con elegancia.
import pytest
from unittest.mock import patch, MagicMock
# parametrize: run one test with multiple inputs
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
])
def test_add_many(a, b, expected):
assert add(a, b) == expected
# parametrize with IDs for readable output
@pytest.mark.parametrize("x", [1, 2, 3], ids=["one", "two", "three"])
def test_ids(x):
assert x > 0
# Mocking: replace external dependencies
def fetch_user(uid):
# imagine this calls a real API
return {"id": uid, "name": "real_user"}
@patch("__main__.fetch_user")
def test_with_mock(mock_fetch):
mock_fetch.return_value = {"id": 1, "name": "mocked"}
result = fetch_user(1)
assert result["name"] == "mocked"
mock_fetch.assert_called_once_with(1)
# Skip and expected failure
@pytest.mark.skip(reason="not implemented yet")
def test_future():
pass
@pytest.mark.xfail(reason="known bug #42")
def test_known_bug():
assert 1 == 2Programación de Red
Servidor TCP
Crea un servidor TCP usando el módulo socket. bind asocia el socket con una dirección, listen establece la cola de backlog, accept bloquea hasta que un cliente se conecta. Cierra siempre las conexiones para liberar descriptores de archivo.
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', 8080))
server.listen(5)
conn, addr = server.accept()
data = conn.recv(1024)
conn.sendall(b'Hello')
conn.close()Cliente TCP
Crea un cliente TCP que se conecta a un servidor. connect establece la conexión, sendall envía todos los bytes, recv lee hasta el número de bytes especificado. Usa encode/decode para conversión cadena-a-bytes.
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 8080))
client.sendall(b'Hello Server')
response = client.recv(1024)
print(response.decode())
client.close()Socket UDP
UDP es sin conexión: sin handshake, sin entrega garantizada. recvfrom devuelve tanto los datos como la dirección del remitente. Usa SOCK_DGRAM para UDP. Ideal para DNS, juegos y streaming en tiempo real.
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('localhost', 9090))
data, addr = sock.recvfrom(1024)
print(f"From {addr}: {data.decode()}")
sock.sendto(b'Reply', addr)Servidor HTTP
El módulo http.server proporciona un servidor HTTP simple. Hereda de BaseHTTPRequestHandler y sobrescribe do_GET, do_POST. Úsalo solo para desarrollo; usa gunicorn para producción.
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(b'<h1>Hello</h1>')
HTTPServer(('localhost', 8000), Handler).serve_forever()Timeout de Socket
settimeout establece un timeout para todas las operaciones de socket. Si una operación excede el timeout, se lanza una excepción socket.timeout. Usa try/finally para garantizar la limpieza.
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
try:
sock.connect(('example.com', 80))
data = sock.recv(1024)
except socket.timeout:
print('Connection timed out')
finally:
sock.close()Base de Datos (SQLite)
Crear Tabla
sqlite3 está integrado en Python. connect crea o abre un archivo de base de datos. CREATE TABLE IF NOT EXISTS previene errores si la tabla existe. Llama siempre a commit para guardar los cambios.
import sqlite3
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER)''')
conn.commit()Insertar Datos
Usa siempre consultas parametrizadas (placeholders ?) para prevenir inyección SQL. lastrowid devuelve el ID autoincrementado. Nunca uses formateo de cadenas para valores SQL.
cursor.execute(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
('Alice', '[email protected]', 30))
conn.commit()
print(f"ID: {cursor.lastrowid}")Consultar Datos
fetchall devuelve todas las filas coincidentes como una lista de tuplas. fetchone devuelve una sola fila o None. Para conjuntos de resultados grandes, itera sobre el cursor directamente.
cursor.execute('SELECT * FROM users WHERE age > ?', (25,))
rows = cursor.fetchall()
for row in rows:
print(row)
cursor.execute('SELECT * FROM users WHERE id = ?', (1,))
user = cursor.fetchone()Actualizar y Borrar
UPDATE modifica filas existentes, DELETE las elimina. rowcount indica las filas afectadas. Usa siempre WHERE con DELETE. commit persiste los cambios.
cursor.execute('UPDATE users SET age = ? WHERE name = ?', (31, 'Alice'))
cursor.execute('DELETE FROM users WHERE age < ?', (18,))
conn.commit()
print(f"Affected: {cursor.rowcount} rows")Row Factory
Usar conn como gestor de contexto hace auto-commit en caso de éxito y rollback en caso de excepción. row_factory = sqlite3.Row permite acceder a columnas por nombre.
conn = sqlite3.connect('example.db')
conn.row_factory = sqlite3.Row
with conn:
conn.execute('INSERT INTO users (name, email) VALUES (?, ?)',
('Bob', '[email protected]'))
for row in conn.execute('SELECT * FROM users'):
print(row['name'], row['email'])Web Scraping
Fundamentos de BeautifulSoup
requests obtiene contenido HTML, BeautifulSoup lo analiza. html.parser está integrado; lxml es más rápido. Comprueba siempre response.status_code antes de analizar.
import requests
from bs4 import BeautifulSoup
resp = requests.get('https://example.com')
soup = BeautifulSoup(resp.text, 'html.parser')
print(soup.title.string)
print(soup.find('h1').text)Encontrar Elementos
find_all devuelve todos los elementos coincidentes, find devuelve el primero. Usa class_ (con guion bajo). select usa selectores CSS para consultas complejas.
links = soup.find_all('a')
for link in links:
print(link.get('href'), link.text)
article = soup.find('div', class_='article')
items = soup.select('ul.list > li.item')Extraer Tablas
Las tablas se estructuran como tr (filas) que contienen celdas td (datos) o th (cabecera). strip elimina espacios en blanco. find_all acepta una lista de nombres de etiqueta.
table = soup.find('table')
for row in table.find_all('tr'):
cols = row.find_all(['td', 'th'])
data = [col.text.strip() for col in cols]
print(data)Manejar Paginación
La paginación se maneja siguiendo enlaces de página siguiente. select_one devuelve la primera coincidencia o None. Añade time.sleep entre peticiones.
all_items = []
url = 'https://example.com/page/1'
while url:
resp = requests.get(url)
soup = BeautifulSoup(resp.text, 'html.parser')
all_items.extend([i.text for i in soup.select('.item')])
next_link = soup.select_one('a.next')
url = next_link.get('href') if next_link else NoneGuardar a CSV
csv.DictWriter escribe diccionarios a CSV. newline previene líneas en blanco extra en Windows. encoding=utf-8 maneja caracteres especiales.
import csv
with open('data.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['name', 'price'])
writer.writeheader()
for item in scraped_data:
writer.writerow(item)Web Asíncrono (aiohttp)
Cliente HTTP
aiohttp proporciona HTTP asíncrono. ClientSession gestiona el pooling de conexiones. async with garantiza la limpieza. asyncio.run ejecuta la corrutina.
import aiohttp, asyncio
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
data = asyncio.run(fetch('https://api.example.com'))Peticiones Concurrentes
asyncio.gather ejecuta corrutinas concurrentemente, reduciendo el tiempo total. Todas las peticiones comparten la misma sesión. Usa un semáforo para limitar la concurrencia.
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [session.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
return [await r.text() for r in responses]Servidor Web
aiohttp.web crea servidores web asíncronos. Las rutas se definen con método HTTP y patrón de ruta. match_info extrae parámetros de ruta.
from aiohttp import web
async def handle(request):
name = request.match_info.get('name', 'World')
return web.json_response({'message': f'Hello, {name}!'})
app = web.Application()
app.add_routes([web.get('/', handle), web.get('/{name}', handle)])
web.run_app(app, port=8080)Servidor WebSocket
Los WebSockets habilitan comunicación bidireccional en tiempo real. WebSocketResponse maneja el handshake de upgrade. async for itera sobre los mensajes.
async def ws_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws.send_str(f'Echo: {msg.data}')
return wsSesión con Cookies
ClientSession persiste cookies entre peticiones automáticamente. Esencial para scraping autenticado. Usa una sola sesión para todas las peticiones.
async def login_and_fetch():
async with aiohttp.ClientSession() as session:
await session.post('https://example.com/login',
data={'user': 'admin', 'pass': '123'})
resp = await session.get('https://example.com/dashboard')
return await resp.text()Multiprocessing en Profundidad
Pool de Procesos
Pool gestiona procesos worker. map distribuye trabajo en paralelo. apply_async ejecuta una sola función de forma asíncrona. Usa siempre la guarda if __name__ == main en Windows.
from multiprocessing import Pool
def square(x): return x * x
if __name__ == '__main__':
with Pool(4) as pool:
results = pool.map(square, range(10))
result = pool.apply_async(square, (100,))
print(result.get(timeout=5))Memoria Compartida
Value y Array crean memoria compartida entre procesos. Usa get_lock para sincronizar el acceso y prevenir condiciones de carrera.
from multiprocessing import Value, Array
counter = Value('i', 0)
arr = Array('d', [0.0, 1.0, 2.0])
with counter.get_lock():
counter.value += 1Comunicación por Queue
Queue habilita comunicación segura entre procesos. put añade elementos, get los recupera. Queue es segura entre procesos, gestionando el locking internamente.
from multiprocessing import Process, Queue
def worker(q):
q.put('Data from worker')
if __name__ == '__main__':
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
print(q.get())
p.join()Pipe
Pipe crea un canal de comunicación bidireccional. send y recv transmiten objetos Python vía pickling. Pipe es más rápido que Queue para comunicación punto a punto.
from multiprocessing import Process, Pipe
def worker(conn):
conn.send(['hello', 'world'])
msg = conn.recv()
conn.close()
if __name__ == '__main__':
parent, child = Pipe()
p = Process(target=worker, args=(child,))
p.start()
print(parent.recv())
parent.send('acknowledged')
p.join()Sincronización
Lock garantiza que solo un proceso acceda a un recurso compartido a la vez. with lock adquiere y libera automáticamente. Otras primitivas: RLock, Semaphore, Event.
from multiprocessing import Process, Lock
def safe_print(lock, msg):
with lock:
print(msg)
if __name__ == '__main__':
lock = Lock()
procs = [Process(target=safe_print, args=(lock, f'Task {i}'))
for i in range(5)]
for p in procs: p.start()
for p in procs: p.join()Entornos Virtuales en Profundidad
Módulo venv
venv crea entornos Python aislados con sus propios directorios de paquetes. La activación modifica PATH. Activa siempre antes de instalar dependencias.
# Create
python -m venv myenv
# Activate (Linux/Mac)
source myenv/bin/activate
# Activate (Windows)
myenv\Scripts\activate
# Deactivate
deactivaterequirements.txt
requirements.txt lista las dependencias del proyecto. == fija versiones exactas, >= permite actualizaciones dentro de un rango. Haz siempre commit al control de versiones.
# Generate
pip freeze > requirements.txt
# Install
pip install -r requirements.txt
# Pin versions
flask==2.3.3
requests>=2.28.0,<3.0.0Poetry
Poetry es un gestor de dependencias moderno. pyproject.toml reemplaza a requirements.txt. Los entornos virtuales se gestionan automáticamente.
# Initialize
poetry init
# Add dependency
poetry add flask
poetry add pytest --group dev
# Install all
poetry install
# Run command
poetry run python app.pypipenv
pipenv combina pip y virtualenv. Pipfile declara dependencias, Pipfile.lock fija versiones exactas. --dev separa las dependencias de desarrollo.
# Create environment
pipenv install
# Add package
pipenv install requests
pipenv install pytest --dev
# Activate shell
pipenv shell
# Run command
pipenv run python app.pyEntornos Conda
Conda gestiona tanto dependencias Python como no Python. environment.yml captura el entorno completo. Ideal para ciencia de datos con dependencias binarias.
# Create
conda create -n myenv python=3.11
# Activate
conda activate myenv
# Export
conda env export > environment.yml
# Recreate
conda env create -f environment.ymlUso Avanzado de pip
Instalar desde Git
Instala paquetes directamente desde repositorios Git. Útil para versiones no publicadas, forks o paquetes privados. @branch o @commit fija a una versión específica.
# From GitHub
pip install git+https://github.com/user/repo.git
# Specific branch
pip install git+https://github.com/user/repo.git@branch-name
# Specific commit
pip install git+https://github.com/user/repo.git@abc123Instalación Editable
La instalación editable (-e) enlaza el paquete en lugar de copiarlo. Los cambios están disponibles inmediatamente sin reinstalar. Esencial para desarrollo de paquetes.
# Install in development mode
pip install -e .
# From a specific path
pip install -e /path/to/package
# With extras
pip install -e ".[dev,test]"Constraints y Hashes
Los constraints limitan qué versiones pueden instalarse. La comprobación de hashes verifica la integridad del paquete, previniendo ataques a la cadena de suministro.
# constraints.txt
flask==2.3.3
pip install -c constraints.txt flask
# Hash checking
pip install --require-hashes -r requirements.txtGestión de Caché
pip cachea los wheels descargados. --no-cache-dir fuerza descargas frescas. Purge libera espacio en disco cuando la caché crece demasiado.
# Show cache info
pip cache info
# List cached packages
pip cache list
# Purge entire cache
pip cache purge
# Install with no cache
pip install --no-cache-dir flaskÍndice Personalizado
--index-url especifica un repositorio de paquetes personalizado. --extra-index-url añade un fallback. --trusted-host omite SSL para registros internos.
# Use custom index
pip install --index-url https://pypi.custom.com/simple/ flask
# Extra index (fallback)
pip install --extra-index-url https://pypi.custom.com/simple/ flask
# Trusted host (no SSL)
pip install --trusted-host pypi.custom.com flaskComprobación de Tipos (mypy)
Anotaciones de Tipo Básicas
Las anotaciones de tipo anotan parámetros de función y tipos de retorno. Python 3.9+ admite tipos integrados directamente. Las anotaciones habilitan análisis estático con mypy.
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
def process(data: list[int]) -> dict[str, int]:
return {str(x): x for x in data}Optional y Union
Optional[X] es equivalente a X | None (Python 3.10+). Los tipos Union admiten múltiples tipos posibles. mypy comprueba que todos los caminos de código manejen todos los tipos.
from typing import Optional
def find(items: list[int], target: int) -> int | None:
for i, v in enumerate(items):
if v == target: return i
return NoneTipos Genéricos
Los genéricos crean contenedores reutilizables type-safe. TypeVar define una variable de tipo, Generic hace la clase genérica. mypy garantiza la consistencia de tipos.
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()Protocol
Protocol define subtipado estructural (duck typing con comprobación de tipos). Cualquier clase con los métodos requeridos satisface el protocolo, sin necesidad de herencia.
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
def cleanup(resource: Closeable) -> None:
resource.close()
class File:
def close(self) -> None: print("Closed")
cleanup(File()) # OK - File has close()Configuración de mypy
mypy.ini configura la rigidez de la comprobación de tipos. strict habilita todas las comprobaciones. Las sobrescrituras por módulo relajan reglas para pruebas o código legacy.
# mypy.ini
[mypy]
python_version = 3.11
strict = True
warn_return_any = True
disallow_untyped_defs = True
[mypy-tests.*]
ignore_errors = TrueConsejos de Rendimiento
Lista vs Generador
Las listas almacenan todos los elementos en memoria; los generadores producen valores bajo demanda. Usa generadores para secuencias grandes iteradas una vez.
# List: all in memory
squares = [x**2 for x in range(1000000)]
# Generator: lazy evaluation
squares_gen = (x**2 for x in range(1000000))
import sys
print(sys.getsizeof(squares)) # ~8MB
print(sys.getsizeof(squares_gen)) # ~200 bytesConcatenación de Cadenas
La concatenación de cadenas con += es O(n^2). join es O(n). Los f-strings son el método de interpolación más rápido.
# Slow: creates intermediate strings
result = ""
for s in parts:
result += s
# Fast: join in one operation
result = "".join(parts)
# Fast: f-strings
msg = f"Hello, {name}!"Variables Locales
Las búsquedas de variables locales son más rápidas que las búsquedas globales o de atributos. Asignar funciones de uso frecuente a locales acelera los bucles.
import math
# Slow: global lookup
def compute_slow(values):
return [math.sqrt(v) for v in values]
# Fast: local reference
def compute_fast(values):
sqrt = math.sqrt
return [sqrt(v) for v in values]__slots__
__slots__ evita la creación de __dict__, ahorrando 40-50% de memoria por instancia. Significativo al crear millones de objetos. No se pueden añadir atributos no listados.
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
# p.z = 3 # AttributeErrortimeit y cProfile
timeit mide el tiempo de ejecución de pequeños fragmentos. cProfile muestra dónde se gasta el tiempo. Usa profiling antes de optimizar para encontrar los cuellos de botella reales.
import timeit
t = timeit.timeit('sum(range(100))', number=10000)
print(f"{t:.4f}s")
import cProfile
cProfile.run('sum(x**2 for x in range(10000))')Errores Comunes
Argumentos por Defecto Mutables
Los valores de argumentos por defecto se evalúan una vez en tiempo de definición. Los defaults mutables se comparten entre todas las llamadas. Usa siempre None como default.
# BUG: default list is shared
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2]!
# FIX: use None
def add_item(item, lst=None):
if lst is None: lst = []
lst.append(item)
return lstClosures con Late Binding
Las closures capturan variables por referencia. Para cuando se llaman las lambdas, la variable del bucle tiene su valor final. Los argumentos por defecto capturan el valor actual.
# BUG: all print 2
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2]
# FIX: default argument
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]Caché de Enteros
Python cachea enteros pequeños. is comprueba identidad, == comprueba igualdad. Nunca uses is para comparación de valores; usa is solo para None, True, False.
# Small integers cached (-5 to 256)
a = 256; b = 256
print(a is b) # True (cached)
c = 257; d = 257
print(c is d) # False (not cached)
print(c == d) # Trueis vs ==
is comprueba si dos referencias apuntan al mismo objeto. == comprueba si dos objetos tienen el mismo valor. Usa is solo para None, True, False.
a = [1, 2, 3]; b = [1, 2, 3]
print(a == b) # True (same values)
print(a is b) # False (different objects)
# Correct usage of is
if x is None: ...
if x is not None: ...GIL
El GIL permite que solo un hilo ejecute bytecode de Python a la vez. El threading es efectivo para tareas limitadas por E/S. Usa multiprocessing para paralelismo limitado por CPU.
# GIL prevents true parallelism for CPU-bound tasks
import threading
def cpu_work():
total = sum(i**2 for i in range(10**7))
# Use multiprocessing for CPU work
from multiprocessing import Pool
with Pool(4) as p:
p.map(cpu_work, range(4))Fragmentos de Python relacionados
Copy-paste ready code for common tasks.
Ordenar Diccionario por Valor
Ordenar un diccionario de Python por sus valores en orden descendente.
Comprensión de Listas
Generar listas rápidamente usando comprensiones de listas.
Fusión de Diccionarios
Múltiples formas de fusionar diccionarios.
Lectura/Escritura de Archivos
Diversas formas de leer y escribir archivos.
Procesamiento de CSV
Leer y escribir archivos CSV usando el módulo csv.
Procesamiento de JSON
Serialización y deserialización JSON.
Coincidencia con Regex
Realizar coincidencia con regex usando el módulo re.
Manejo de Fechas
Manejar fechas y horas con datetime.
Decoradores
Definir y usar decoradores.
Generadores
Ahorrar memoria usando generadores.
Context Manager
Context managers personalizados.
Manejo de Excepciones
Mecanismo completo de manejo de excepciones.
Herencia de Clases
Herencia de clases y sobreescritura de métodos.
Multihilo
Implementar multihilo usando el módulo threading.
Multiproceso
Lograr paralelismo real con multiprocessing.
Programación Asíncrona con asyncio
Implementar concurrencia asíncrona con asyncio.
Programación con Sockets
Servidor y cliente TCP Socket.
Peticiones HTTP
Enviar peticiones HTTP usando la librería requests.
Operaciones de Base de Datos
Operar sobre bases de datos usando sqlite3.
Entorno Virtual
Crear y gestionar entornos virtuales de Python.
Instalación con pip
Comandos comunes de gestión de paquetes con pip.
Variables de Entorno
Leer y establecer variables de entorno.
Logging
Configurar y usar el módulo logging.
Pruebas Unitarias
Escribir pruebas unitarias usando unittest.
Type Hints
Mejorar la legibilidad del código con anotaciones de tipo.
Dataclass
Simplificar definiciones de clases con dataclass.
Enum
Definir tipos enum usando Enum.
Decorador property
Controlar el acceso a atributos con property.
Métodos Mágicos
Ejemplos comunes de métodos mágicos.
Iterador
Implementación de iterador personalizado.
Corrutina
Uso básico de corrutinas.
Was this helpful?