🔎 Buscar

🐍 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.

Wiki / Apuntes📖 Contenido

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: Edad y edad son variables distintas.
  • No hay const: la convención es NOMBRE_MAYUSCULA para 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.2 da 0.30000000000000004 por la representación binaria. Para dinero o precisión exacta usa decimal.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/case para encadenados de igualdad (se ve en Python moderno), pero if/elif/else sigue 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 lanza KeyError. 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)

💡 frozenset es 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 def con 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 requiere global: 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 devuelve str. Convertir a int/float con un input inválido lanza ValueError: envuélvelo en try/except si 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, olvidar f.close() deja el archivo abierto: se pierde el buffer y se bloquean recursos. with es 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 e encadena excepciones cuando relanzas desde un except, 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 install global 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.