🐍 Fundamentos de Python
Sintaxis, tipos de datos, control de flujo, colecciones, funciones, strings, entrada y salida, archivos, excepciones, módulos y virtualenv. La base completa de Python.
Fundamentos de Python
Python es un lenguaje interpretado, de tipado dinámico y sintaxis legible que prioriza la productividad. Este artículo cubre la base completa: desde cómo se ejecuta un programa hasta módulos, excepciones y entornos virtuales. Si dominas esto, el resto de la ruta (OOP, async, moderno) es solo añadir capas.
Sintaxis y semántica
Python usa indentación para delimitar bloques (en vez de llaves) y : al final de las cabeceras. El estilo de referencia es PEP 8: 4 espacios por nivel, sin tabuladores.
edad = 25
if edad >= 18:
print("Mayor de edad") # este bloque pertenece al if
print("Puede votar")
print("Siempre se ejecuta") # fuera del bloque
- Los comentarios van con
#. Los docstrings con"""...""". - Los nombres son case-sensitive:
Edadyedadson variables distintas. - No hay
const: la convención esNOMBRE_MAYUSCULApara constantes. - La semántica es dinámica: una variable puede cambiar de tipo en tiempo de ejecución.
valor = 10
valor = "ahora soy texto" # válido: el tipo lo decide el valor, no la variable
💡 Python es de duck typing: “si camina como pato y suena como pato, es un pato”. El tipo importa por comportamiento, no por declaración. Los type hints (tema de Python moderno) son anotaciones, no obligaciones en runtime.
Tipos de datos básicos
| Tipo | Ejemplo | Notas |
|---|---|---|
int |
42, -7, 1_000_000 |
Enteros de precisión arbitraria |
float |
3.14, 2.0, 1e-3 |
Punto flotante (IEEE 754) |
bool |
True, False |
Subclase de int |
str |
"hola", 'mundo' |
Inmutable, secuencia de caracteres |
None |
None |
Valor nulo, único objeto de su tipo |
print(type(42)) # <class 'int'>
print(type(3.14)) # <class 'float'>
print(type(True)) # <class 'bool'>
print(type(None)) # <class 'NoneType'>
print(int("10")) # conversión: 10
print(float(3)) # 3.0
print(bool(0)) # False
print(bool("")) # False
print(bool("texto")) # True
⚠️ Cuidado con las operaciones float:
0.1 + 0.2da0.30000000000000004por la representación binaria. Para dinero o precisión exacta usadecimal.Decimal.
None es el valor centinela que indica “sin valor”. Es falsy: en condiciones booleales cuenta como falso.
Operadores
# Aritméticos
print(10 + 3) # 13
print(10 - 3) # 7
print(10 * 3) # 30
print(10 / 3) # 3.333... (siempre float)
print(10 // 3) # 3 (división entera)
print(10 % 3) # 1 (módulo/resto)
print(10 ** 3) # 1000 (potencia)
# Comparación (devuelven bool)
print(10 == 10) # True
print(10 != 3) # True
print(10 > 3) # True
print(3 <= 3) # True
# Lógicos
print(True and False) # False
print(True or False) # True
print(not True) # False
# Asignación aumentada
x = 5
x += 2 # x = 7
x *= 3 # x = 21
Los operadores de comparación se pueden encadenar: 1 < x < 10 es válido y más legible que x > 1 and x < 10.
Control de flujo
if / elif / else
nota = 85
if nota >= 90:
print("A")
elif nota >= 80:
print("B")
elif nota >= 70:
print("C")
else:
print("Reprobado")
💡 Desde Python 3.10 existe
match/casepara encadenados de igualdad (se ve en Python moderno), peroif/elif/elsesigue siendo la herramienta para condiciones arbitrarias.
for
El for itera sobre iterables, no sobre índices:
frutas = ["manzana", "pera", "uva"]
for fruta in frutas:
print(fruta)
# Con índice cuando lo necesitas
for i, fruta in enumerate(frutas):
print(f"{i}: {fruta}")
# Rango numérico
for n in range(1, 6): # 1..5 (el final es exclusivo)
print(n)
# Dict: claves, valores o pares
precios = {"pan": 3, "leche": 2}
for clave, valor in precios.items():
print(clave, "->", valor)
while
contador = 0
while contador < 5:
print(contador)
contador += 1
# break sale del bucle; continue pasa a la siguiente iteración
for n in range(10):
if n == 2:
continue # se salta el 2
if n == 7:
break # corta en el 7
print(n)
# else en bucles: se ejecuta si NO hubo break
for n in range(3):
if n == 99:
break
else:
print("No hubo break: bucle completado")
Listas
Las listas son mutables y ordenadas. La estructura de datos más usada.
frutas = ["manzana", "pera"]
frutas.append("uva") # añade al final
frutas.insert(1, "kiwi") # inserta en índice 1
frutas.remove("pera") # elimina por valor (primera coincidencia)
elemento = frutas.pop() # saca el último y lo devuelve
elemento = frutas.pop(0) # saca por índice
frutas.sort() # ordena in-place
frutas.reverse() # invierte in-place
print(frutas.count("uva")) # cuántas veces aparece
print(frutas.index("uva")) # índice de la primera coincidencia
print("uva" in frutas) # pertenencia: True
Slicing
secuencia[inicio:fin:paso] — el final es exclusivo:
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[2:5]) # [2, 3, 4]
print(nums[:4]) # [0, 1, 2, 3] (desde el inicio)
print(nums[6:]) # [6, 7, 8, 9] (hasta el final)
print(nums[::2]) # [0, 2, 4, 6, 8] (cada 2)
print(nums[::-1]) # [9, 8, ..., 0] (invertida)
print(nums[-3:]) # [7, 8, 9] (índices negativos desde el final)
💡 El slicing siempre crea una copia. Modificar un slice no afecta a la lista original. Para copiar una lista:
copia = original[:](copia superficial).
Tuplas
Las tuplas son inmutables (no se pueden modificar). Sirven para datos fijos y como claves de dict.
punto = (3, 4)
print(punto[0]) # 3
print(punto[1]) # 4
# Unpacking: desempaquetar
x, y = punto
print(x, y) # 3 4
# Tupla de un solo elemento: ¡necesita coma!
una = (5,)
print(type(una)) # <class 'tuple'>
# Intercambio de variables
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1
Diccionarios
Los dict mapean claves → valores. Claves: cualquier valor inmutable (str, int, tuple).
persona = {"nombre": "Ana", "edad": 30, "ciudad": "Madrid"}
print(persona["nombre"]) # Ana
print(persona.get("pais", "España")) # get con valor por defecto
persona["email"] = "ana@mail.com" # añadir o actualizar
persona["edad"] = 31 # actualizar
print(persona.keys()) # vista de claves
print(persona.values()) # vista de valores
print(persona.items()) # pares (clave, valor)
# Eliminar
del persona["ciudad"] # por clave (KeyError si no existe)
persona.pop("email", None) # elimina y devuelve, con default
# Construcción
d1 = dict(nombre="Luis", edad=25)
d2 = {k: v for k, v in [("a", 1), ("b", 2)]}
# Anidación
config = {"db": {"host": "localhost", "port": 5432}}
print(config["db"]["port"]) # 5432
⚠️ Acceder con
[]a una clave inexistente lanzaKeyError. Usa.get(clave, default)cuando no estés seguro.
Sets
Los set son colecciones sin duplicados y sin orden. Ideales para pertenencia, eliminar repetidos y operaciones de conjuntos.
conjunto = {1, 2, 3}
conjunto.add(4)
conjunto.add(2) # no hace nada: ya existe
conjunto.discard(5) # elimina sin error si no existe
conjunto.remove(3) # elimina; KeyError si no existe
# Eliminar duplicados
lista = [1, 1, 2, 3, 3, 3]
unicos = list(set(lista)) # [1, 2, 3]
a = {1, 2, 3}
b = {2, 3, 4}
print(a | b) # unión: {1, 2, 3, 4}
print(a & b) # intersección: {2, 3}
print(a - b) # diferencia: {1}
print(a ^ b) # diferencia simétrica: {1, 4}
print(a.issubset({1, 2, 3, 4})) # True
print(2 in a) # pertenencia O(1)
💡
frozensetes la versión inmutable: sirve como clave de dict y elemento de otro set.
Comprensiones
Una comprensión construye una colección a partir de un iterable en una línea, con filtro opcional.
# List comprehension: [expresión for elemento in iterable if condición]
cuadrados = [n ** 2 for n in range(10)] # [0, 1, 4, 9, ...]
pares = [n for n in range(20) if n % 2 == 0] # solo pares
palabras = ["hola", "mundo", "python"]
mayus = [p.upper() for p in palabras if len(p) > 3]
# Set comprehension
cuadrados_unicos = {n % 5 for n in range(20)} # {0, 1, 2, 3, 4}
# Dict comprehension
cuadrados_dict = {n: n ** 2 for n in range(5)} # {0: 0, 1: 1, 2: 4, ...}
# Comprensión anidada (matriz → filas planas)
matriz = [[1, 2], [3, 4]]
plana = [num for fila in matriz for num in fila] # [1, 2, 3, 4]
# Alternativa legible con if/else dentro de la expresión
signos = ["+" if n > 0 else "-" for n in [-1, 2, -3, 4]] # ['-', '+', '-', '+']
💡 La compresión mental:
[expresión for x in iterable if condición]se lee “aplica expresión a cada x, filtrando por condición”. Todo lo que cabe en un for anidado cabe aquí, pero si cuesta leerlo, mejor un for normal.
Funciones
def saludar(nombre):
"""Devuelve un saludo personalizado."""
return f"Hola, {nombre}"
print(saludar("Marta")) # Hola, Marta
# Argumentos por defecto
def potencia(base, exponente=2):
return base ** exponente
print(potencia(3)) # 9 (usa el default)
print(potencia(3, 4)) # 81
# Parámetros por nombre
def perfil(nombre, edad, ciudad):
print(nombre, edad, ciudad)
perfil("Ana", 30, "Madrid")
perfil(ciudad="Madrid", nombre="Ana", edad=30) # orden libre con keywords
# *args: número variable de argumentos posicionales → tupla
def suma_total(*numeros):
return sum(numeros)
print(suma_total(1, 2, 3, 4)) # 10
# **kwargs: número variable de keywords → dict
def configurar(**opciones):
print(opciones)
configurar(host="localhost", puerto=8080) # {'host': 'localhost', 'puerto': 8080}
Lambdas
Las lambdas son funciones anónimas de una sola expresión:
doble = lambda x: x * 2
print(doble(5)) # 10
# El uso más común: como argumento de orden superior
pares_ordenados = sorted([(1, "b"), (2, "a")], key=lambda p: p[1])
print(pares_ordenados) # [(2, 'a'), (1, 'b')]
nums = [1, 2, 3, 4]
impares = list(filter(lambda n: n % 2 == 1, nums)) # [1, 3]
cuadrados = list(map(lambda n: n ** 2, nums)) # [1, 4, 9, 16]
💡 Prefiere una función
defcon nombre si la lambda tiene más de una expresión o necesitas reutilizarla. Las lambdas son para lógica trivial de una línea.
Scope: LEGB
Python busca un nombre en este orden: Local, Enclosing, Global, Built-in.
contador = 0 # global
def incrementar():
contador = 10 # LOCAL: no toca la global
return contador
incrementar()
print(contador) # 0 (la global sigue intacta)
Para modificar una variable global o la de una función exterior, se usan global y nonlocal:
contador = 0
def incrementar():
global contador
contador += 1
incrementar()
print(contador) # 1
def exterior():
valor = 5
def interior():
nonlocal valor # modifica la 'valor' de exterior
valor += 1
interior()
return valor
print(exterior()) # 6
⚠️ Mutar el contenido de una lista/dict global (
.append,d[k] = v) no requiereglobal: solo se necesita para reasignar el nombre de la variable.
Strings
Las cadenas son inmutables: cada “modificación” crea una nueva.
texto = " Hola Mundo "
print(texto.strip()) # 'Hola Mundo' (quita espacios laterales)
print(texto.lower()) # ' hola mundo '
print(texto.upper()) # ' HOLA MUNDO '
print(texto.split()) # ['Hola', 'Mundo']
print(" ".join(["a", "b", "c"]))# 'a b c'
print(texto.replace("Mundo", "Python")) # ' Hola Python '
print(texto.find("Mundo")) # 7 (índice; -1 si no existe)
print(len(texto)) # 14
print("ola" in texto) # True
print(texto.startswith(" ")) # True
print(texto.title()) # ' Hola Mundo '
# Slicing aplica igual que en listas
palabra = "Python"
print(palabra[0:2]) # 'Py'
print(palabra[::-1]) # 'nohtyP'
f-strings y format
Los f-strings (3.6+) son la forma recomendada de interpolar:
nombre = "Luis"
edad = 30
print(f"{nombre} tiene {edad} años")
print(f"{edad:03d}") # '030' (relleno de ceros)
print(f"{3.14159:.2f}") # '3.14' (dos decimales)
print(f"{1234567:,}") # '1,234,567'
print(f"{0.25:.1%}") # '25.0%'
print(f"{nombre:>10}") # ' Luis' (alinear a la derecha)
El método .format() es el antecesor, aún vigente en código legacy:
print("{} tiene {} años".format("Luis", 30))
print("{nombre} tiene {edad}".format(nombre="Luis", edad=30))
Entrada y salida
nombre = input("¿Cómo te llamas? ") # siempre devuelve str
edad = int(input("¿Tu edad? ")) # convertir explícitamente
print(f"Hola, {nombre}, tienes {edad} años")
# print con separadores y fin personalizados
print("a", "b", "c", sep="-") # a-b-c
print("sin salto", end=" -> ")
print("sigue aquí") # sin salto -> sigue aquí
⚠️
input()siempre devuelvestr. Convertir aint/floatcon un input inválido lanzaValueError: envuélvelo entry/exceptsi el dato viene de un usuario real.
Manejo de archivos
La forma correcta es with, que cierra el archivo automáticamente aunque haya un error:
# Escribir
with open("datos.txt", "w", encoding="utf-8") as f:
f.write("línea uno\n")
f.write("línea dos\n")
# Leer todo
with open("datos.txt", "r", encoding="utf-8") as f:
contenido = f.read()
print(contenido)
# Leer línea por línea (eficiente con archivos grandes)
with open("datos.txt", "r", encoding="utf-8") as f:
for linea in f:
print(linea.strip())
# Modos: 'r' lectura, 'w' escritura (sobrescribe), 'a' append, 'r+' lectura/escritura
with open("datos.txt", "a", encoding="utf-8") as f:
f.write("línea extra\n")
# JSON (el formato de intercambio estándar)
import json
datos = {"nombre": "Ana", "hobbies": ["leer", "correr"]}
with open("datos.json", "w", encoding="utf-8") as f:
json.dump(datos, f, ensure_ascii=False, indent=2)
with open("datos.json", "r", encoding="utf-8") as f:
cargado = json.load(f)
print(cargado["nombre"]) # Ana
💡 Sin
with, olvidarf.close()deja el archivo abierto: se pierde el buffer y se bloquean recursos.withes el patrón obligatorio en Python moderno.
Excepciones
Python levanta excepciones cuando algo sale mal. Se manejan con try/except:
try:
numero = int(input("Número: "))
print(10 / numero)
except ValueError:
print("Eso no es un número válido")
except ZeroDivisionError:
print("No puedes dividir entre cero")
Jerarquía importante: ZeroDivisionError y ValueError son hijas de ArithmeticError y Exception respectivamente; capturar Exception atrapa casi todo.
else y finally
try:
archivo = open("no_existe.txt", "r")
except FileNotFoundError:
print("El archivo no existe")
else:
# se ejecuta SOLO si no hubo excepción
print(archivo.read())
archivo.close()
finally:
# se ejecuta SIEMPRE (limpieza garantizada)
print("Fin del intento")
raise y excepciones personalizadas
def validar_edad(edad):
if edad < 0:
raise ValueError("La edad no puede ser negativa")
return edad
# Excepciones personalizadas: heredar de Exception
class SaldoInsuficienteError(Exception):
"""Se lanza cuando no hay saldo para una operación."""
class CuentaBancaria:
def __init__(self, saldo):
self.saldo = saldo
def retirar(self, cantidad):
if cantidad > self.saldo:
raise SaldoInsuficienteError(
f"Quieres {cantidad} pero solo hay {self.saldo}"
)
self.saldo -= cantidad
cuenta = CuentaBancaria(100)
try:
cuenta.retirar(200)
except SaldoInsuficienteError as e:
print(f"Error: {e}")
💡
raise ... from eencadena excepciones cuando relanzas desde unexcept, preservando el contexto original para el debugging.
Módulos e import
Un módulo es un archivo .py. Un paquete es una carpeta con __init__.py que agrupa módulos.
import math # módulo completo
from datetime import datetime # importar algo concreto
from collections import OrderedDict, deque
import os.path as osp # alias
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.14159...
print(datetime.now())
# import relativo dentro de un paquete
# from . import helper # mismo paquete
# from ..models import Usuario # paquete padre
Buenas prácticas de import (PEP 8):
- Primero los de la librería estándar, luego los de terceros, luego los propios.
- Importar módulos, no símbolos sueltos cuando haya colisión de nombres.
- Evitar
from x import *: ensucia el namespace.
La biblioteca estándar que debes conocer de memoria:
import os # rutas y archivos del sistema
import sys # argumentos y runtime
import json # JSON
import re # regex
import collections # defaultdict, Counter, deque
import itertools # combinaciones, product, chain
import random # aleatoriedad
import statistics # mean, median
import pathlib # rutas modernas con Path
virtualenv y pip
Las dependencias se instalan con pip dentro de un entorno virtual (venv), que aísla los paquetes de cada proyecto.
# Crear el entorno (una sola vez por proyecto)
python -m venv .venv
# Activar
# Windows (PowerShell):
.venv\Scripts\Activate.ps1
# macOS / Linux:
source .venv/bin/activate
# Instalar dependencias
pip install requests
pip install fastapi "uvicorn[standard]"
# Ver lo instalado
pip list
pip freeze # salida compatible con requirements.txt
# Guardar / restaurar dependencias
pip freeze > requirements.txt
pip install -r requirements.txt
⚠️ NUNCA instales dependencias de proyecto con
pip installglobal fuera de un venv: corrompes paquetes entre proyectos. El venv es obligatorio desde Python 3.3 y la recomendación oficial.
Alternativas modernas: uv y poetry resuelven y congelan dependencias con más rigor. requirements.txt sigue siendo el estándar mínimo que todos entienden.
Cheatsheet
| Quieres… | Usas… |
|---|---|
| Colección mutable ordenada | list |
| Colección inmutable | tuple |
| Clave → valor | dict |
| Sin duplicados | set |
| Colección derivada en una línea | comprensiones [...] {...} {k: v} |
| Cortar una secuencia | sec[inicio:fin:paso] |
| N args posicionales | *args |
| N keywords | **kwargs |
| Modificar variable global | global x |
| Función anónima | lambda |
| Interpolar texto | f"...{var}..." |
| Leer de consola | input() |
| Archivos seguros | with open(...) as f: |
| Manejar errores | try/except/else/finally |
| Aislar dependencias | python -m venv .venv |
Para profundizar
- The Python Tutorial (docs.python.org): el tutorial oficial, la mejor primera lectura.
- Python Standard Library: la referencia de todos los módulos incluidos.
- PEP 8 — Style Guide: las convenciones de estilo oficiales.
- Real Python — Learn Python: cientos de tutoriales prácticos por tema.
- Fluent Python (Luciano Ramalho): el libro de referencia de Python en profundidad.
- Siguiente paso: OOP en Python.