Clase 2 — Variables y tipos básicos
La documentación oficial de Python lo dice sin rodeos: "Objects are Python's abstraction for data. All data in a Python program is represented by objects or by relations between objects. Even code is represented by objects."
Un número, un texto, una función, un módulo importado — todo es un objeto. Y todo objeto tiene exactamente tres propiedades, definidas en el momento de su creación:
— 1. Identidad —
La identidad es la dirección en memoria donde
vive el objeto. Nunca cambia una vez creado el objeto.
La función id() la devuelve como número entero.
El operador is compara identidades — no valores.
Documentación oficial: "An object's identity never changes once it has been created; you may think of it as the object's address in memory." → fuente
# Identidad: id() devuelve la dirección en memoria
x = 42
print(id(x))
y = 42
print(id(y))
print(x is y) # ¿mismo objeto o distinto?
— 2. Tipo —
El tipo determina qué operaciones puede hacer el objeto
y qué valores puede tener. Tampoco cambia nunca.
La función type() lo devuelve — y es en sí mismo un objeto.
Documentación oficial: "An object's type determines the operations that the object supports and also defines the possible values for objects of that type. Like its identity, an object's type is also unchangeable." → fuente
# Tipo: type() devuelve el tipo del objeto
print(type(42)) # int
print(type(3.14)) # float
print(type("hola")) # str
print(type(True)) # bool
print(type(type(42))) # ¿qué devuelve esto?
— 3. Valor y mutabilidad —
El valor es el contenido del objeto. A diferencia de la identidad y el tipo, el valor puede cambiar en algunos objetos. Los que pueden cambiar se llaman mutables; los que no, inmutables.
Números, strings y booleanos son inmutables. Listas y diccionarios son mutables. Esto no es una convención — es una propiedad del tipo, definida por el lenguaje.
Documentación oficial: "Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. An object's mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable." → fuente
# Inmutable: no puedes cambiar el objeto, solo redirigir la etiqueta
x = "hola"
print(id(x))
x = "hola mundo" # ¿mismo id o distinto?
print(id(x))
# Mutable: el objeto sí cambia
lista = [1, 2, 3]
print(id(lista))
lista.append(4) # modificamos el objeto
print(id(lista)) # ¿mismo id?
print(lista)
uv run jupyter lab en tu terminal
y ejecuta cada bloque en una celda nueva (Shift + Enter). ¿x e
y
tienen el mismo id? ¿Qué dice x is y?
¿Por qué Python puede reutilizar el mismo objeto para dos
variables distintas con el mismo valor?
type(type(42)). ¿Qué devuelve?
¿Qué te dice eso sobre cómo trata Python al propio
concepto de "tipo"?
id de x
cuando reasignas el string? ¿Y el id de lista
cuando haces append? ¿Qué diferencia fundamental
muestra esto entre objetos mutables e inmutables?
x = 42 y luego
x = "hola" sin error. ¿Hay contradicción?
¿Qué ha cambiado exactamente?
En la mayoría de implementaciones de Python,
x e y tienen el mismo id
y x is y devuelve True.
Esto ocurre porque Python optimiza memoria reutilizando
objetos inmutables pequeños muy frecuentes — los enteros
entre -5 y 256 están precreados y no se duplican.
Dos variables con valor 42 apuntan
al mismo objeto en memoria.
x = 1000; y = 1000) el comportamiento
puede ser distinto. Por eso nunca debes usar is
para comparar valores — usa siempre ==.
is compara identidades, no valores.
type(type(42)) devuelve <class 'type'>.
El tipo de 42 es int, y el tipo
de int es type. En Python,
incluso los tipos son objetos — instancias de la clase
type. No hay ningún elemento del lenguaje
que quede fuera del modelo de objetos.
El id de x cambia al reasignar
el string: no has modificado el objeto "hola",
has creado un objeto nuevo "hola mundo"
y has redirigido la etiqueta. El objeto original
sigue existiendo en memoria sin cambios.
El id de lista no cambia
al hacer append: es el mismo objeto
en la misma dirección de memoria, pero su valor
ha cambiado — ahora contiene cuatro elementos.
Eso es precisamente lo que significa ser mutable.
No hay contradicción. El tipo del objeto 42
nunca cambia: sigue siendo int para siempre.
Lo que ha cambiado es que la etiqueta x
ya no apunta a ese objeto — ahora apunta al objeto
"hola", que es un str.
Son dos objetos distintos con tipos distintos.
La variable x no tiene tipo propio.
El tipo lo tiene el objeto, no el nombre.
x es simplemente una etiqueta que puede
redirigirse a cualquier objeto en cualquier momento.
Esto es el tipado dinámico de Python.
Lo que sigue no es necesario entenderlo todavía. La idea es que veas, antes de seguir, hasta dónde llega la afirmación de que "todo dato en Python está representado por objetos". No son solo los números y los strings — también las funciones, los módulos importados y las estructuras de datos. Todo sigue el mismo modelo: identidad, tipo, valor.
— Las funciones son objetos —
Una función no es solo una instrucción que se ejecuta. Es un objeto con identidad y tipo, igual que un número o un string. Puedes asignarla a una variable, pasarla como argumento, o guardarla en una lista. Lo veremos en detalle más adelante.
— Los módulos son objetos —
Cuando escribes import math, Python crea un objeto
de tipo module y le pone la etiqueta math.
Ese objeto contiene funciones, constantes y otros objetos dentro.
Los módulos y los paquetes los veremos en detalle más adelante.
import math
print(type(math)) # module
print(id(math)) # tiene identidad
print(type(math.sqrt)) # la función sqrt también es un objeto
print(math.pi) # incluso las constantes son objetos
En muchos lenguajes de programación, una variable es una caja: reservas un espacio en memoria, le pones un nombre, y metes un valor dentro. El valor vive dentro de la caja.
En Python es diferente. Una variable es una etiqueta — o más precisamente, un name binding: un nombre que apunta a un objeto que ya existe en memoria. El objeto vive en memoria independientemente de cuántas etiquetas apunten a él.
La documentación oficial sostiene:
las variables no son contenedores —
son referencias a objetos.
El operador = no significa
"mete este valor en esta caja".
Significa "haz que este nombre apunte
a este objeto".
— Lo que ocurre en memoria —
Cuando escribes x = 42 ocurren dos cosas:
Python crea el objeto 42 en memoria
y luego crea la etiqueta x que apunta a él.
El objeto existe primero — la etiqueta viene después.
Cuando escribes x = 43, Python no modifica
el objeto 42. Crea un objeto nuevo 43
y mueve la etiqueta x para que apunte al nuevo.
El objeto 42 sigue en memoria — sin etiquetas
que lo referencien, el recolector de basura lo eliminará
en algún momento.
# Paso 1: Python crea el objeto 42 y la etiqueta x apunta a él
x = 42
print(id(x)) # dirección del objeto 42 en memoria
# Paso 2: Python crea el objeto 43 y mueve la etiqueta
x = 43
print(id(x)) # dirección diferente: es un objeto nuevo
# El objeto 42 sigue existiendo — compruébalo
print(id(42)) # el objeto 42 tiene su propio id
print(id(43)) # el objeto 43 también
Observa también lo que ocurre cuando dos etiquetas apuntan al mismo objeto: es como que dos flechas apuntan al mismo bloque en memoria:
# Dos etiquetas, un solo objeto
x = 42
y = x # y no copia el valor — apunta al mismo objeto
print(id(x))
print(id(y))
print(x is y) # True: mismo objeto, no solo mismo valor
— Ejercicio —
id de x
al pasar de 42 a 43?
¿Qué nos dice eso sobre qué ha cambiado
y qué no?
x e y
tienen el mismo id?
Ahora escribe x = 99 y vuelve a imprimir
id(x) e id(y).
¿Qué le ha pasado a y?
¿Por qué?
Sí, el id cambia. Al escribir
x = 43 no has modificado el objeto
42 — ese objeto sigue en memoria
con su propia identidad. Lo que ha cambiado
es que la etiqueta x ahora apunta
a un objeto diferente. La etiqueta se ha movido,
el objeto no.
Sí, x e y tienen
el mismo id inicialmente:
apuntan al mismo objeto 42.
Cuando escribes x = 99, la etiqueta
x se mueve al objeto 99,
pero y no se mueve — sigue apuntando
al objeto 42.
Cada etiqueta es independiente. Reasignar una etiqueta nunca afecta a las demás, porque lo que has cambiado es a qué objeto apunta esa etiqueta, no el objeto en sí.
La diferencia práctica es significativa cuando lees código con funciones o con estructuras de datos. Si una función recibe una variable como argumento, no recibe una copia del valor — recibe una referencia al mismo objeto. Si ese objeto es mutable, la función puede modificarlo y el cambio se verá fuera de la función.
Entender que las variables son referencias es lo que te permite predecir este tipo de comportamiento sin ejecutar el código. Es una de las habilidades de lectura más importantes en Python.
=: evaluar, crear,
apuntar
teoría
En matemáticas, x = x + 1 es una contradicción:
ningún número es igual a sí mismo más uno.
En Python, x = x + 1 es una instrucción
perfectamente válida — pero no significa lo mismo.
El símbolo = en Python no es igualdad.
Es el operador de asignación,
y funciona siempre en tres pasos, en este orden:
Evalúa el lado derecho.
Python lee todo lo que hay a la derecha del =
y calcula el resultado. Todavía no ha tocado nada
a la izquierda.
Crea un objeto nuevo en memoria con el resultado de ese cálculo. Ese objeto tiene su propia identidad — su propia dirección en memoria.
Mueve la etiqueta del lado izquierdo para que apunte al objeto nuevo. El objeto anterior sigue en memoria — simplemente ya no tiene esta etiqueta apuntándole.
— ¿x = x + 1 crea un objeto nuevo o modifica el anterior? —
Esta es la pregunta clave. Cuando Python ejecuta
x = x + 1, ¿mueve la etiqueta x
a un nuevo objeto, o modifica el objeto al que ya apuntaba?
La respuesta la da hex(id(x)) — la dirección
de memoria en hexadecimal, que es como normalmente
se representan las direcciones y es más fácil de comparar
visualmente que un número entero de 15 dígitos.
Si el id cambia después de la operación,
x apunta a un objeto nuevo.
Si no cambia, el objeto fue modificado en su lugar.
x = 10
print(f"Antes: x = {x}, id = {hex(id(x))}")
x = x + 1
print(f"Después: x = {x}, id = {hex(id(x))}")
# ¿El id cambió? ¿Qué nos dice eso?
El id cambia. Esto confirma que
x + 1 ha creado un objeto nuevo —
el 11 — y la etiqueta x
se ha movido a ese objeto nuevo.
El objeto 10 sigue en memoria,
sin etiquetas que lo referencien,
esperando a ser recogido por el recolector de basura.
Esto ocurre porque los números en Python son inmutables: no se pueden modificar. Cualquier operación aritmética produce siempre un objeto nuevo.
— La forma abreviada: += —
x += 1 es exactamente lo mismo que
x = x + 1 — es solo una forma más corta
de escribirlo. Python la llama
operador de asignación aumentada.
Mismos tres pasos, mismo resultado, mismo cambio de id.
x = 10
print(f"Antes: x = {x}, id = {hex(id(x))}")
x += 1 # equivale a x = x + 1
print(f"Después: x = {x}, id = {hex(id(x))}")
x *= 2 # equivale a x = x * 2
print(f"Después: x = {x}, id = {hex(id(x))}")
— El lado derecho se evalúa completo antes de asignar —
Python siempre evalúa el lado derecho entero antes de tocar el lado izquierdo. Esto hace posible el intercambio elegante de dos variables en una sola línea — algo que en otros lenguajes requiere una variable temporal.
a = 10
b = 20
print(f"Antes: a = {a}, b = {b}")
# Python evalúa (b, a) completo primero → (20, 10)
# luego asigna: a = 20, b = 10
a, b = b, a
print(f"Después: a = {a}, b = {b}")
— Asignación múltiple: ¿una etiqueta o dos? —
Python permite encadenar asignaciones en una sola línea.
x = y = 0 no crea dos objetos 0
— crea uno solo y pone dos etiquetas apuntando a él.
Verifica con hex(id()).
x = y = 0
print(f"x = {x}, id = {hex(id(x))}")
print(f"y = {y}, id = {hex(id(y))}")
print(f"¿Mismo objeto? {x is y}")
# ¿Qué pasa si ahora cambias x?
x = 1
print(f"\nDespués de x = 1:")
print(f"x = {x}, id = {hex(id(x))}")
print(f"y = {y}, id = {hex(id(y))}") # ¿y ha cambiado?
id
después de x = x + 1?
Explica con tus propias palabras qué ha pasado
en memoria en los tres pasos.
+= y *=.
¿Cambia el id en cada operación?
¿Qué confirma eso sobre la inmutabilidad de los números?
a, b = b, a,
¿qué pasaría si Python evaluara el lado izquierdo
primero? ¿Por qué el resultado sería incorrecto?
x = y = 0,
después de hacer x = 1, ¿ha cambiado
el valor de y? ¿Por qué?
¿Qué pasaría si en lugar de 0
hubieras asignado una lista mutable?
Sí, el id cambia. Los tres pasos
en memoria son: primero Python evalúa
x + 1 tomando el valor del objeto
al que apunta x (el 10)
y sumándole 1. Segundo, crea
un objeto nuevo 11 en una dirección
de memoria distinta. Tercero, mueve la etiqueta
x para que apunte a ese objeto nuevo.
El objeto 10 no ha sido modificado
— sigue existiendo en memoria hasta que
el recolector de basura lo elimine.
Sí, el id cambia en cada operación.
+= y *= son azúcar
sintáctico — formas abreviadas que hacen
exactamente lo mismo que x = x + 1
y x = x * 2. En cada caso
se crea un objeto nuevo y la etiqueta se mueve.
Esto confirma que los números son inmutables:
no existe ninguna operación que modifique
un objeto numérico en su lugar.
Si Python evaluara el lado izquierdo primero,
en a, b = b, a primero asignaría
a = b (con lo que a
valdría 20), y luego intentaría
b = a — pero ahora a
ya vale 20, así que b
también quedaría en 20.
El intercambio fallaría.
Que Python evalúe el lado derecho completo
primero — (b, a) = (20, 10)
— es lo que hace posible el intercambio elegante.
No, y no cambia. Al hacer
x = 1, la etiqueta x
se mueve al objeto 1, pero
la etiqueta y sigue apuntando
al objeto 0 — cada etiqueta
es independiente.
Con una lista mutable sería diferente y peligroso.
Si haces x = y = [] y luego
x.append(1), tanto x
como y verían el cambio —
porque ambas etiquetas apuntan al mismo objeto
y ese objeto ha sido modificado en su lugar.
Lo veremos en detalle en el Bloque 4.
En el Bloque 1 vimos que todo objeto tiene tres propiedades: identidad, tipo y valor. El tipo es la más importante para el trabajo diario porque determina qué operaciones puede hacer el objeto.
La documentación oficial lo dice explícitamente: "An object's type determines the operations that the object supports and also defines the possible values for objects of that type." → fuente
Esto tiene una consecuencia directa para las variables: cuando asignas un objeto a una variable, la etiqueta apunta a ese objeto y hereda implícitamente todas sus capacidades y limitaciones. Saber de qué tipo es el objeto al que apunta una variable es lo que te permite predecir qué puedes hacer con ella y qué no.
— Descubre el tipo antes de que te lo digamos —
Para cada bloque de abajo: antes de ejecutar,
intenta predecir qué tipo devolverá type().
Luego ejecuta en JupyterLab y comprueba.
La función type() es tu herramienta
de inspección — úsala siempre que leas código ajeno
y no estés seguro de qué tipo de objeto estás manejando.
— Números —
# Asignamos objetos numéricos a variables
# ¿De qué tipo es cada uno?
ventas_enero = 42000
tasa_iva = 0.21
activo = True
fecha = "2024-01-15"
print(type(ventas_enero))
print(type(tasa_iva))
print(type(activo))
print(type(fecha)) # ¿sorprende este resultado?
La última línea suele sorprender: "2024-01-15"
es una fecha para nosotros, pero para Python es un
str — texto. Python no sabe que esos
números separados por guiones representan una fecha.
Para trabajar con fechas como fechas hay que
convertirlas explícitamente. Si no lo haces,
Python las tratará como texto y no podrás
calcular diferencias de días ni ordenarlas
cronológicamente.
— Estructuras de datos —
# Distintas formas de agrupar datos
lista_ventas = [100, 200, 150, 300]
coordenadas = (40.4168, -3.7038)
alumno = {"nombre": "Ana", "nota": 8.5}
print(type(lista_ventas))
print(type(coordenadas))
print(type(alumno))
— Objetos de librerías de análisis de datos —
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Un array de numpy
array_ventas = np.array([100, 200, 150, 300])
print(type(array_ventas)) # ¿mismo tipo que lista_ventas?
# Un DataFrame de pandas
df = pd.DataFrame({
"mes": ["ene", "feb", "mar"],
"ventas": [100, 200, 150]
})
print(type(df)) # ¿qué tipo es un DataFrame?
print(type(df["ventas"])) # ¿y una columna del DataFrame?
# Un gráfico
fig = plt.figure()
print(type(fig)) # incluso un gráfico es un objeto
plt.close()
— Cuando los tipos no son compatibles —
El tipo de un objeto no es solo información descriptiva — es lo que Python usa para decidir si una operación tiene sentido o no. Cuando intentas operar sobre objetos de tipos incompatibles, Python lanza un error. Estos errores son visibles y fáciles de corregir.
Pero hay un caso más peligroso: cuando la operación no lanza error pero produce un resultado incorrecto porque el tipo no era el que esperabas. Lo veremos en los ejemplos de abajo.
— Error visible: tipos incompatibles —
# Una fecha como string + un número: ¿qué pasa?
fecha = "2024-01-15"
dias = 30
# Intenta sumar: ¿qué error produce?
print(fecha + dias)
Python lanza un TypeError: no puede sumar
un str y un int.
El error es claro y fácil de detectar.
La solución sería convertir la fecha a un objeto
de tipo datetime antes de operar.
— Error silencioso: el tipo no es el que esperabas —
import pandas as pd
# Simulamos datos de ventas donde el campo numérico
# llegó como texto — algo muy común con datos reales
df = pd.DataFrame({
"mes": ["ene", "feb", "mar"],
"ventas": ["100", "200", "150"] # strings, no números
})
# ¿Qué tipo tiene la columna ventas?
print(type(df["ventas"]))
print(df["ventas"].dtype)
# Calculamos la media — ¿funciona?
print(df["ventas"].mean()) # ¿qué resultado obtienes?
# Comparamos con la versión correcta
df["ventas_num"] = df["ventas"].astype(int)
print(df["ventas_num"].mean()) # ahora sí
Este es el error más peligroso: mean()
no lanza ningún error cuando la columna es de tipo
object (string en pandas) — simplemente
devuelve NaN o un resultado incorrecto
sin avisar. Si no verificas el tipo de la columna
antes de operar, puedes publicar resultados incorrectos
sin saberlo.
Este tipo de error — datos numéricos que llegan
como strings — es uno de los más frecuentes
en análisis de datos reales. La primera línea
de defensa es siempre verificar el tipo
con type() o df.dtypes.
— Lista vs array: mismo aspecto, comportamiento distinto —
import numpy as np
lista = [1, 2, 3]
array = np.array([1, 2, 3])
# Multiplicar por 2: ¿mismo resultado?
print(lista * 2) # ¿qué hace esto con una lista?
print(array * 2) # ¿y con un array?
# Sumar dos colecciones: ¿mismo resultado?
print(lista + [4, 5]) # concatena
print(array + np.array([4, 5, 6])) # suma elemento a elemento
El operador * sobre una lista repite sus elementos —
[1, 2, 3] * 2 produce [1, 2, 3, 1, 2, 3].
Sobre un array de numpy multiplica cada elemento por 2
— [2, 4, 6]. El mismo operador, el mismo símbolo,
comportamientos completamente distintos según el tipo.
Esto es exactamente lo que decía la documentación oficial:
el tipo determina las operaciones que soporta el objeto.
Cuando lees código ajeno y ves un *,
necesitas saber el tipo del objeto para saber
qué está haciendo realmente.
df["ventas"].mean()?
¿Produce un error o un resultado silenciosamente
incorrecto? ¿Qué tiene que ver esto con el
error de nivel 2 que vimos en la Clase 1?
lista * 2 y array * 2.
¿Producen el mismo resultado?
¿Qué nos dice esto sobre la importancia de saber
el tipo de un objeto antes de operar con él?
precio = "9.99"
cantidad = 3
total = precio * cantidad
print(f"Total: {total} euros")
La sorpresa habitual es fecha:
intuitivamente parece una fecha, pero Python
la ve como str — texto.
También puede sorprender activo = True
siendo de tipo bool, que es
un subtipo de int en Python:
True == 1 y False == 0.
En versiones recientes de pandas,
df["ventas"].mean() devuelve
NaN sin lanzar ningún error.
Es un error silencioso — exactamente el
error de nivel 2 de la Clase 1: el código
ejecuta, produce un resultado, y ese resultado
es incorrecto sin ningún mensaje de aviso.
La única defensa es verificar el tipo
antes de operar: df.dtypes
o df["ventas"].dtype.
No producen el mismo resultado.
lista * 2 repite los elementos:
[1, 2, 3, 1, 2, 3].
array * 2 multiplica cada elemento:
[2, 4, 6].
El mismo operador hace cosas radicalmente distintas
según el tipo. Cuando lees código ajeno que usa
operadores aritméticos sobre colecciones,
necesitas saber el tipo antes de interpretar
qué está haciendo el código.
precio es str,
cantidad es int.
La operación precio * cantidad
no falla — produce "9.993":
Python repite el string tres veces.
El print mostrará
"Total: 9.993 euros"
sin ningún error, pero el resultado
es completamente incorrecto.
precio = float("9.99").
Verificar siempre el tipo con type()
cuando los datos vienen de fuentes externas.
En el Bloque 3 vimos que el tipo va con el objeto, no con la variable. Esto tiene un nombre: tipado dinámico. En Python no declaras el tipo de una variable antes de usarla — el tipo se determina en el momento de la asignación, cuando la etiqueta apunta a un objeto concreto.
Contrasta con lenguajes de tipado estático como Java o C, donde tienes que declarar el tipo antes de usarlo y no puedes cambiarlo:
Java — tipado estático
int x = 42; // tipo declarado
x = "hola"; // ERROR en compilación
Python — tipado dinámico
x = 42 # int
x = "hola" # str — sin error
El tipado dinámico tiene una ventaja clara: flexibilidad — puedes escribir código más rápido sin preocuparte por declaraciones. Pero tiene un riesgo igual de claro: puedes perder el control de qué tipo de objeto apunta tu etiqueta en cada momento, especialmente en programas largos o en código generado por IA que no has escrito tú.
— El tipo de una variable puede cambiar a lo largo del programa —
# Una variable que cambia de tipo varias veces
resultado = 0
print(f"1. resultado = {resultado}, tipo = {type(resultado)}")
resultado = resultado + 3.14
print(f"2. resultado = {resultado}, tipo = {type(resultado)}")
resultado = f"El valor es {resultado}"
print(f"3. resultado = {resultado}, tipo = {type(resultado)}")
resultado = [resultado, resultado]
print(f"4. resultado = {resultado}, tipo = {type(resultado)}")
La misma etiqueta resultado apunta sucesivamente
a un int, un float, un str
y una list. Python no se queja en ningún momento.
Pero si intentaras hacer resultado / 2
después del paso 3, obtendrías un TypeError
porque en ese momento resultado apunta
a un string, no a un número.
Este patrón — una variable que cambia de tipo sin que el código lo señalice claramente — es muy común en código generado por IA y es una fuente habitual de errores difíciles de detectar.
— Un caso real: función que devuelve tipos distintos —
import pandas as pd
def buscar_venta(df, mes):
resultado = df[df["mes"] == mes]["ventas"]
if len(resultado) == 0:
return None # NoneType
elif len(resultado) == 1:
return resultado.iloc[0] # int o float
else:
return resultado # Series de pandas
df = pd.DataFrame({
"mes": ["ene", "feb", "mar"],
"ventas": [100, 200, 150]
})
v1 = buscar_venta(df, "ene")
v2 = buscar_venta(df, "abr") # no existe
print(type(v1), v1)
print(type(v2), v2)
Esta función devuelve tipos distintos según el caso:
None, un número, o una Serie de pandas.
La variable v1 apunta a objetos de tipo
distinto dependiendo de qué mes se busca.
Si después intentas hacer v2 + 10
sin verificar el tipo primero, obtienes un error
porque v2 es None.
Este patrón es extremadamente común en código real
y en código generado por IA. La única defensa
es verificar el tipo antes de operar:
if v2 is not None:.
— Anotaciones de tipo: documentar sin restringir —
Python es dinámicamente tipado, pero desde Python 3.5 puedes añadir anotaciones de tipo opcionales. Son una forma de documentar qué tipo esperas que tenga una variable o qué tipo devuelve una función.
Importante: Python no verifica
las anotaciones en tiempo de ejecución.
No son restricciones — son documentación para humanos
e IDEs. Si pones x: int = 42
y luego haces x = "hola",
Python no lanza ningún error.
Las verás constantemente en código generado por IA y en librerías modernas. Saber leerlas es imprescindible.
— Sintaxis básica en variables —
# Sin anotación — Python clásico
precio = 9.99
cantidad = 3
# Con anotación — documenta la intención
precio: float = 9.99
cantidad: int = 3
nombre: str = "Ana"
activo: bool = True
# Python no verifica — esto no da error
precio: float = "nueve con noventa y nueve"
print(type(precio)) # str, no float
— Sintaxis en funciones: lo que verás en código real —
# Sin anotaciones
def calcular_total(precio, cantidad):
return precio * cantidad
# Con anotaciones — mucho más legible
# precio: float → espera un float
# cantidad: int → espera un int
# -> float → devuelve un float
def calcular_total(precio: float, cantidad: int) -> float:
return precio * cantidad
# Funciona igual — las anotaciones no cambian el comportamiento
print(calcular_total(9.99, 3))
print(calcular_total("9.99", 3)) # ¿error o resultado inesperado?
La última línea no produce error — produce
"9.999.999.99": Python repite el string
tres veces porque str * int es concatenación.
La anotación precio: float no impidió
que pasáramos un string. Eso es el tipado dinámico:
flexible pero silenciosamente peligroso.
— Código generado por IA: leer anotaciones —
# Típico código generado por IA — ¿puedes leer qué hace?
def procesar_ventas(
datos: pd.DataFrame,
columna: str,
umbral: float = 0.0
) -> pd.Series:
"""Filtra y devuelve ventas por encima del umbral."""
return datos[datos[columna] > umbral][columna]
# ¿Qué tipo espera cada parámetro?
# ¿Qué tipo devuelve?
# ¿Qué pasa si pasas una lista en lugar de un DataFrame?
resultado
cambia de tipo cuatro veces. Después de cada
asignación, ¿qué operación fallaría?
Por ejemplo: después del paso 3, ¿qué pasa
si intentas resultado / 2?
buscar_venta,
¿qué tipo devuelve la función cuando el mes
no existe? ¿Qué error producirá
v2 + 10 y por qué?
¿Cómo lo evitarías antes de operar?
precio: float = "nueve con noventa",
¿qué devuelve type(precio)?
¿Qué nos dice eso sobre la naturaleza
de las anotaciones en Python?
procesar_ventas
generada por IA. Sin ejecutarla:
¿qué tipo espera el parámetro datos?
¿Qué devuelve? ¿Qué valor tiene umbral
si no lo especificas al llamar a la función?
Después del paso 1 (int):
resultado.upper() fallaría —
los enteros no tienen métodos de string.
Después del paso 2 (float):
resultado.split() fallaría —
los floats tampoco.
Después del paso 3 (str):
resultado / 2 fallaría con
TypeError — no puedes dividir
un string.
Después del paso 4 (list):
resultado + 1 fallaría —
no puedes sumar un número a una lista.
Cuando el mes no existe, la función devuelve
None — de tipo NoneType.
v2 + 10 produce un TypeError:
"unsupported operand type(s) for +:
'NoneType' and 'int'".
La forma de evitarlo es verificar antes de operar:
if v2 is not None:
print(v2 + 10)
else:
print("Mes no encontrado")
type(precio) devuelve
<class 'str'>, no float.
La anotación : float no hizo nada —
Python la ignoró completamente en tiempo
de ejecución. Las anotaciones son documentación,
no restricciones. Si quieres verificación real
necesitarías herramientas externas como
mypy o activar la verificación
en tu IDE.
datos espera un pd.DataFrame.
columna espera un str
— el nombre de la columna a filtrar.
umbral espera un float
y tiene valor por defecto 0.0,
así que si no lo especificas la función
devuelve todas las ventas mayores que cero.
La función devuelve un pd.Series.
En el Bloque 1 vimos que todo objeto tiene una propiedad llamada mutabilidad: algunos objetos pueden cambiar su valor después de ser creados (mutables) y otros no (inmutables). Ahora que entendemos las variables como etiquetas, podemos ver por qué esto importa tanto en la práctica.
El modelo de etiquetas crea una situación que sorprende a casi todo el mundo la primera vez: dos etiquetas pueden apuntar al mismo objeto. Si ese objeto es inmutable, no hay problema — cualquier operación crea un objeto nuevo. Si ese objeto es mutable, modificarlo desde una etiqueta afecta a todas las demás que apuntan a él.
— Con objetos inmutables: sin sorpresas —
Los números, strings y tuplas son inmutables. Cualquier operación sobre ellos crea un objeto nuevo. Por eso, reasignar una etiqueta nunca afecta a las demás: cada etiqueta se mueve de forma independiente.
# Dos etiquetas apuntando al mismo número
a = 42
b = a
print(f"Antes — a: {a}, b: {b}")
print(f"Mismo objeto: {a is b}, id(a): {hex(id(a))}, id(b): {hex(id(b))}")
# Reasignamos a — ¿cambia b?
a = 99
print(f"\nDespués — a: {a}, b: {b}")
print(f"Mismo objeto: {a is b}, id(a): {hex(id(a))}, id(b): {hex(id(b))}")
b no cambia. Al hacer a = 99,
la etiqueta a se mueve al objeto 99,
pero b sigue apuntando al objeto 42.
El id de b no ha variado.
Con objetos inmutables, las etiquetas son completamente
independientes.
En el Bloque 1 vimos que todo objeto tiene una propiedad llamada mutabilidad: algunos objetos pueden cambiar su valor después de ser creados (mutables) y otros no (inmutables). Ahora que entendemos las variables como etiquetas, podemos ver por qué esto importa tanto en la práctica.
El modelo de etiquetas crea una situación que sorprende a casi todo el mundo la primera vez: dos etiquetas pueden apuntar al mismo objeto. Si ese objeto es inmutable, no hay problema — cualquier operación crea un objeto nuevo. Si ese objeto es mutable, modificarlo desde una etiqueta afecta a todas las demás que apuntan a él.
— Con objetos inmutables: sin sorpresas —
Los números, strings y tuplas son inmutables. Cualquier operación sobre ellos crea un objeto nuevo. Por eso, reasignar una etiqueta nunca afecta a las demás: cada etiqueta se mueve de forma independiente.
# Dos etiquetas apuntando al mismo número
a = 42
b = a
print(f"Antes — a: {a}, b: {b}")
print(f"Mismo objeto: {a is b}, id(a): {hex(id(a))}, id(b): {hex(id(b))}")
# Reasignamos a — ¿cambia b?
a = 99
print(f"\nDespués — a: {a}, b: {b}")
print(f"Mismo objeto: {a is b}, id(a): {hex(id(a))}, id(b): {hex(id(b))}")
b no cambia. Al hacer a = 99,
la etiqueta a se mueve al objeto 99,
pero b sigue apuntando al objeto 42.
El id de b no ha variado.
Con objetos inmutables, las etiquetas son completamente
independientes.
— Con objetos mutables: la sorpresa —
Las listas, diccionarios y conjuntos son mutables. Cuando dos etiquetas apuntan al mismo objeto mutable, modificarlo desde una etiqueta afecta a la otra. No porque estén "conectadas" — sino porque ambas apuntan al mismo objeto en memoria, y ese objeto ha cambiado en su lugar.
# Predice antes de ejecutar: ¿qué vale b después?
a = [1, 2, 3]
b = a
print(f"Antes — a: {a}, b: {b}")
print(f"Mismo objeto: {a is b}")
print(f"id(a): {hex(id(a))}, id(b): {hex(id(b))}")
# Modificamos a — ¿cambia b?
a.append(4)
print(f"\nDespués de a.append(4):")
print(f"a: {a}")
print(f"b: {b}") # ¿qué esperas?
print(f"id(a): {hex(id(a))}, id(b): {hex(id(b))}")
b también cambia — aunque no hayas
tocado b en ningún momento.
Los id de a y b
siguen siendo idénticos: es el mismo objeto.
append no crea un objeto nuevo —
modifica el objeto existente en su lugar.
Ambas etiquetas lo ven porque ambas apuntan al mismo sitio.
— Reasignar vs modificar: la diferencia clave —
# Predice: ¿qué vale b en cada caso?
# Caso 1: reasignación
a = [1, 2, 3]
b = a
a = [10, 20, 30] # reasignamos a — nueva lista
print(f"Caso 1 — a: {a}, b: {b}")
# Caso 2: modificación
a = [1, 2, 3]
b = a
a[0] = 99 # modificamos el objeto — mismo id
print(f"Caso 2 — a: {a}, b: {b}")
En el Caso 1, a = [10, 20, 30] crea
una lista nueva y mueve la etiqueta a
a ese nuevo objeto. b sigue apuntando
a la lista original [1, 2, 3] — no cambia.
En el Caso 2, a[0] = 99 modifica
el objeto existente en su lugar — no crea uno nuevo.
b apunta al mismo objeto, así que
también ve el cambio: b es ahora
[99, 2, 3].
a = algo_nuevo mueve la etiqueta —
no afecta a otras etiquetas.
a[0] = 99 o a.append(4)
modifica el objeto — afecta a todas las etiquetas
que apuntan a él.
— La solución: copy() crea un objeto nuevo —
Si necesitas una copia independiente de una lista,
usa .copy(). Esto crea un objeto nuevo
con su propio id — modificar uno
no afecta al otro.
# Sin copy: mismo objeto
a = [1, 2, 3]
b = a
print(f"Sin copy — mismo objeto: {a is b}")
# Con copy: objeto nuevo
a = [1, 2, 3]
b = a.copy()
print(f"Con copy — mismo objeto: {a is b}")
print(f"id(a): {hex(id(a))}, id(b): {hex(id(b))}")
# Ahora modificar a no afecta a b
a.append(4)
print(f"\nDespués de a.append(4):")
print(f"a: {a}")
print(f"b: {b}") # b no cambia
.copy() crea una copia superficial:
los elementos de la lista se copian por referencia.
Si la lista contiene otras listas dentro,
esas listas internas siguen siendo el mismo objeto.
Para copias completamente independientes existe
copy.deepcopy() — lo veremos
cuando trabajemos con estructuras de datos anidadas.
b después
de a.append(4).
Luego ejecuta y verifica con hex(id()).
¿El id de a cambió?
¿Por qué?
b
y en cuál no? ¿Qué diferencia hay entre
a = [10, 20, 30]
y a[0] = 99 en términos
de lo que ocurre en memoria?
copy().
Compara los id de a
y b con y sin copy().
¿Qué confirma eso sobre lo que hace
copy() en memoria?
original
al final?
original = [10, 20, 30]
procesado = original
procesado.append(40)
procesado[0] = 99
print(original)
b vale [1, 2, 3, 4]
— aunque no hayas tocado b.
El id de a no cambia
porque append modifica el objeto
en su lugar — no crea uno nuevo.
Ambas etiquetas siguen apuntando al mismo
objeto en memoria, y ese objeto ahora
contiene cuatro elementos.
En el Caso 1, b no cambia:
sigue siendo [1, 2, 3].
a = [10, 20, 30] crea un objeto
nuevo y mueve la etiqueta a
— b no se mueve.
En el Caso 2, b sí cambia:
pasa a ser [99, 2, 3].
a[0] = 99 modifica el objeto
existente — el id no cambia
y b ve la modificación
porque apunta al mismo objeto.
id
cambia, se ha creado un objeto nuevo y las demás
etiquetas no se ven afectadas. Si el id
no cambia, el objeto ha sido modificado en su lugar
y todas las etiquetas lo ven.
Con copy(), los id
de a y b son distintos:
son dos objetos diferentes en memoria
con el mismo contenido inicial.
Modificar uno no afecta al otro
porque ya no comparten identidad.
original vale [99, 20, 30, 40].
procesado = original no crea
una copia — hace que procesado
apunte al mismo objeto que original.
Todas las modificaciones sobre procesado
— el append y el cambio del primer
elemento — se aplican al objeto compartido,
que también es original.
df_procesado = df_original
y luego modificar df_procesado
pensando que df_original está
protegido. No lo está — es el mismo objeto.
La solución es df_procesado = df_original.copy().
Anticipo — ¿qué es un view?
En NumPy y pandas existe un concepto relacionado con todo lo que acabamos de ver: el view. Un view no es una copia — es una ventana al mismo objeto en memoria. Cuando creas un view de un array o un DataFrame, no estás creando un objeto nuevo: estás creando una etiqueta que apunta a una porción del objeto original.
Esto tiene exactamente las mismas consecuencias que hemos visto con las listas: modificar el view modifica el original, porque ambos comparten el mismo espacio en memoria.
¿Para qué sirve entonces? Para eficiencia: si tienes un array de un millón de filas y solo necesitas trabajar con las primeras mil, un view no duplica los datos en memoria — simplemente apunta a esa porción. Es más rápido y consume menos memoria. Pero hay que saber cuándo tienes un view y cuándo tienes una copia, o los resultados pueden sorprenderte.
Lo veremos en detalle cuando trabajemos con NumPy y pandas en clases posteriores. Por ahora, quédate con la idea: un view es una etiqueta que apunta a una parte de otro objeto — el mismo modelo que hemos visto hoy, aplicado a estructuras de datos grandes.
int
y
float:
números enteros y decimales
teoría
Python distingue entre dos tipos numéricos principales.
Los enteros (int) representan
números sin parte decimal — pueden ser arbitrariamente
grandes, limitados solo por la memoria disponible.
Los decimales (float)
representan números con parte decimal — internamente
se almacenan en formato de punto flotante de doble
precisión, lo que introduce limitaciones de precisión
que conviene conocer.
La documentación oficial los define como objetos inmutables: una vez creados, su valor no cambia. Cualquier operación aritmética produce un objeto nuevo. → fuente
— Crear y verificar tipos numéricos —
# int: enteros sin límite de tamaño
alumnos = 27
poblacion_españa = 47_400_000 # _ como separador visual
numero_grande = 10 ** 100 # un googol — sin problema
print(type(alumnos))
print(type(poblacion_españa))
print(numero_grande)
# float: decimales en punto flotante
tasa_iva = 0.21
pi = 3.14159
notacion_cientifica = 1.5e10 # 1.5 × 10^10
print(type(tasa_iva))
print(notacion_cientifica)
— Operadores aritméticos —
Python tiene siete operadores aritméticos.
Lo más importante para la lectura de código:
el resultado de combinar un int
con un float siempre es un float.
Y la división / siempre devuelve
un float, incluso si el resultado
es exacto.
a = 17
b = 5
print(a + b, type(a + b)) # suma
print(a - b, type(a - b)) # resta
print(a * b, type(a * b)) # multiplicación
print(a / b, type(a / b)) # división real — siempre float
print(a // b, type(a // b)) # división entera — descarta decimal
print(a % b, type(a % b)) # módulo — resto de la división
print(a ** b, type(a ** b)) # potencia
— La trampa de los floats: precisión limitada —
Los float se almacenan en binario,
lo que hace que algunos decimales no puedan
representarse exactamente. Esto no es un bug de Python
— es una limitación del estándar IEEE 754
que afecta a todos los lenguajes de programación.
Es especialmente importante en análisis financiero
y en comparaciones de igualdad.
# Predice antes de ejecutar
print(0.1 + 0.2) # ¿0.3?
print(0.1 + 0.2 == 0.3) # ¿True o False?
# La solución: round() para comparaciones
print(round(0.1 + 0.2, 10) == round(0.3, 10))
# O usar el módulo decimal para cálculos financieros
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))
— Operadores de comparación: producen bool —
Los operadores de comparación toman dos números
y devuelven un bool — True
o False. Son el puente entre los tipos
numéricos y el tipo booleano que veremos en el Bloque 3.
Aparecen constantemente en código real: en condiciones
if, en filtros de pandas, en bucles.
ventas = 150
umbral = 100
print(ventas > umbral, type(ventas > umbral)) # mayor que
print(ventas < umbral) # menor que
print(ventas >= umbral) # mayor o igual
print(ventas <= umbral) # menor o igual
print(ventas == umbral) # igual
print(ventas != umbral) # distinto
# Comparaciones encadenadas — exclusivo de Python
nota = 7.5
print(5 <= nota < 9) # ¿aprobado pero no sobresaliente?
17 / 5?
¿Y 17 // 5? ¿Qué diferencia
práctica tiene esto cuando necesitas
un índice entero para acceder a una lista?
0.1 + 0.2
y si 0.1 + 0.2 == 0.3 es
True o False.
¿Te sorprende el resultado?
¿Qué implicación tiene esto si estás
calculando precios o importes financieros?
17 % 5?
¿Para qué sirve el operador módulo
en análisis de datos?
Pista: piensa en cómo detectar
si un número es par o impar.
x = 10
y = 3.0
print(type(x + y))
print(type(x // y))
print(x > y)
print(type(x > y))
17 / 5 devuelve 3.4
de tipo float — siempre,
incluso cuando el resultado es exacto:
10 / 2 devuelve 5.0,
no 5.
17 // 5 devuelve 3
de tipo int — descarta el decimal.
Cuando necesitas un índice entero para acceder
a una lista — lista[i] —
el índice debe ser int, no float.
Si usas / para calcularlo,
obtendrás un TypeError.
Usa // en ese caso.
0.1 + 0.2 devuelve
0.30000000000000004, no 0.3.
Y 0.1 + 0.2 == 0.3 es False.
La implicación financiera es directa:
nunca uses float para acumular
importes monetarios en un bucle —
el error de precisión se acumula.
Usa Decimal o trabaja
con céntimos como enteros.
17 % 5 devuelve 2:
el resto de dividir 17 entre 5.
En análisis de datos el módulo tiene
varios usos prácticos: detectar pares
(n % 2 == 0), agrupar
datos en bloques de tamaño fijo,
o calcular posiciones cíclicas
en una secuencia.
type(x + y) es float:
mezclar int y float
siempre produce float.
type(x // y) también es float:
aunque sea división entera, si uno de los operandos
es float el resultado es float
— en este caso 3.0, no 3.
x > y es True
y su tipo es bool —
los operadores de comparación siempre
devuelven un booleano.
int y float
juntos, Python siempre promociona el resultado
a float — el tipo más general.
Es una de las reglas más importantes para
leer código numérico sin ejecutarlo.
str:
cadenas de texto
teoría
Un str es una secuencia inmutable
de caracteres Unicode. Inmutable significa
que no puedes cambiar un carácter dentro de un string
existente — cualquier operación que "modifica" un string
crea un objeto nuevo. Unicode significa que puede contener
cualquier carácter de cualquier idioma — letras, números,
emojis, caracteres especiales.
En análisis de datos los strings aparecen en todas partes: nombres de columnas, categorías, fechas mal formateadas, códigos de producto, mensajes de error. Saber leer operaciones sobre strings es imprescindible.
— Crear strings: tres formas —
# Comillas simples, dobles o triples — el resultado es el mismo
s1 = 'hola'
s2 = "hola"
s3 = """hola
mundo""" # string multilínea
print(type(s1), s1 == s2) # mismo tipo, mismo valor
print(s3) # conserva los saltos de línea
# Las comillas triples son útiles para docstrings
def calcular(x):
"""Esta función calcula algo.""" # docstring
return x * 2
print(type(calcular.__doc__)) # el docstring es un str
— Strings como secuencias: indexación y longitud —
Un string es una secuencia — puedes acceder a cada
carácter por su posición usando corchetes.
La primera posición es 0, no 1.
Las posiciones negativas cuentan desde el final.
Esto se llama indexación y funciona
igual en listas, tuplas y cualquier otra secuencia.
ciudad = "Madrid"
print(len(ciudad)) # longitud: 6
print(ciudad[0]) # primer carácter: 'M'
print(ciudad[-1]) # último carácter: 'd'
print(ciudad[1:4]) # slice: 'adr'
print(ciudad[:3]) # primeros 3: 'Mad'
print(ciudad[3:]) # desde pos 3: 'rid'
# Inmutabilidad: no puedes cambiar un carácter
try:
ciudad[0] = "m" # ¿qué error produce?
except TypeError as e:
print(f"Error: {e}")
— Operadores sobre strings —
# Concatenación con +
nombre = "Ana"
apellido = "García"
nombre_completo = nombre + " " + apellido
print(nombre_completo)
print(type(nombre_completo))
# Repetición con *
separador = "-" * 30
print(separador)
# Pertenencia con in
print("Ana" in nombre_completo) # True
print("Pedro" in nombre_completo) # False
# Comparación — compara lexicográficamente
print("ana" == "Ana") # ¿True o False?
print("a" < "b") # orden alfabético
— f-strings: la forma moderna de formatear —
Los f-strings (desde Python 3.6) son la forma
más limpia de construir strings con variables.
Permiten insertar expresiones Python directamente
dentro del texto usando {}.
Los verás constantemente en código moderno
y en código generado por IA.
producto = "manzanas"
cantidad = 5
precio = 2.5
# Forma antigua — concatenación
msg1 = "He comprado " + str(cantidad) + " " + producto
print(msg1)
# Forma moderna — f-string
msg2 = f"He comprado {cantidad} {producto} por {precio:.2f} euros"
print(msg2)
# Expresiones dentro del f-string
msg3 = f"Total: {cantidad * precio:.2f} euros"
print(msg3)
# Alineación y formato
print(f"{'Producto':15} {'Precio':>8}")
print(f"{'Manzanas':15} {2.50:>8.2f}")
print(f"{'Naranjas':15} {1.80:>8.2f}")
— Métodos de strings: operaciones frecuentes —
Los strings tienen métodos propios — funciones
que pertenecen al objeto y se llaman con la notación
objeto.metodo(). Todos devuelven
un string nuevo — recuerda que los strings son inmutables.
Estos son los que más verás en análisis de datos:
texto = " Ventas del Mes de Enero "
# Limpieza — muy común con datos de CSVs
print(texto.strip()) # elimina espacios al inicio y final
print(texto.strip().lower()) # todo en minúsculas
print(texto.strip().upper()) # todo en mayúsculas
# Búsqueda y reemplazo
print(texto.strip().replace("Enero", "Febrero"))
# División — muy útil para parsear datos
fecha = "2024-01-15"
partes = fecha.split("-")
print(partes) # ['2024', '01', '15']
print(type(partes)) # ¿qué tipo devuelve split?
# Verificación
codigo = "ABC123"
print(codigo.isalpha()) # ¿solo letras?
print(codigo.isdigit()) # ¿solo dígitos?
print(codigo.startswith("ABC")) # ¿empieza por ABC?
"2024" es un string, no un entero.
"2024" + 1 produce un TypeError.
"2024" + "01" produce "202401",
no 2025.
Esto es uno de los errores más frecuentes cuando
los datos vienen de un CSV o una API —
los números llegan como strings y hay que convertirlos
explícitamente con int() o float().
ciudad[0] = "m"?
¿Cómo crearías un string nuevo con la primera
letra en minúscula sin modificar el original?
"ana" == "Ana".
¿Por qué? ¿Cómo lo resolverías si necesitas
comparar nombres independientemente
de mayúsculas y minúsculas?
fecha.split("-")?
¿Cómo accederías solo al año?
¿Y cómo lo convertirías a entero?
valor = " 42.5 "
print(type(valor))
print(valor.strip())
print(type(valor.strip()))
print(float(valor.strip()))
print(type(float(valor.strip())))
Produce un TypeError:
"'str' object does not support item assignment".
Los strings son inmutables — no puedes cambiar
un carácter en su lugar.
Para crear un string con la primera letra
en minúscula sin modificar el original:
nuevo = ciudad[0].lower() + ciudad[1:]
print(nuevo) # "madrid"
print(ciudad) # "Madrid" — sin cambios
O más elegante: ciudad.lower()
convierte todo a minúsculas,
ciudad[0].lower() + ciudad[1:]
solo la primera letra.
"ana" == "Ana" es False.
Python distingue mayúsculas de minúsculas
en la comparación de strings — "a"
y "A" son caracteres distintos.
Para comparar independientemente de mayúsculas:
print("ana".lower() == "Ana".lower()) # True
Es un patrón muy frecuente en limpieza de datos: normalizar todo a minúsculas antes de comparar o agrupar categorías.
split() devuelve una list
— una lista de strings.
Para acceder al año: partes[0],
que devuelve el string "2024".
Para convertirlo a entero:
int(partes[0]),
que devuelve el entero 2024.
año = int(fecha.split("-")[0])
— partir una fecha como string y extraer
el componente que necesitas como entero.
type(valor) → str.
valor.strip() → "42.5"
— sigue siendo str, solo
sin espacios.
type(valor.strip()) → str.
float(valor.strip()) → 42.5
— ahora sí es un número.
type(float(valor.strip())) → float.
float(valor.strip()) es
la forma estándar de convertir un número
que llegó como string con espacios
a un float operable.
Es una de las líneas más frecuentes
en pipelines de limpieza de datos reales.
bool:
el tipo que controla toda la lógica de un programa
teoría
El tipo bool tiene solo dos valores posibles:
True y False.
Es el tipo más pequeño de Python — pero es
el mecanismo que controla toda la lógica
de cualquier programa.
Cada vez que Python decide si ejecutar un bloque
de código o no — cada if, cada while,
cada filtro de pandas, cada condición en un bucle —
está evaluando algo a True o False.
Sin entender booleanos no puedes leer ningún programa real,
por sencillo que sea.
La documentación oficial añade algo que sorprende: "The Boolean type is a subtype of the integer type, and Boolean values behave like the values 0 and 1, respectively, in almost all contexts." → fuente
— bool como subtipo de int: True == 1, False == 0 —
bool es un subtipo de int.
Esto no es una curiosidad — tiene consecuencias prácticas
muy concretas. True se comporta como
1 y False como 0
en operaciones aritméticas. Esto permite contar
cuántos elementos cumplen una condición con sum()
— uno de los patrones más frecuentes en análisis de datos.
# bool es subtipo de int
print(type(True))
print(type(False))
print(isinstance(True, int)) # ¿True es un int?
# True == 1, False == 0
print(True == 1)
print(False == 0)
print(True + True) # predice el resultado
print(True + 5) # ¿y este?
print(False * 100) # ¿y este?
# Contar cuántos elementos cumplen una condición
notas = [4.5, 7.0, 3.0, 8.5, 5.0, 9.0, 4.0]
aprobados = [nota >= 5 for nota in notas]
print(aprobados) # lista de booleanos
print(sum(aprobados)) # cuenta los True — ¿cuántos aprobados?
— Operadores lógicos: and, or, not —
Los tres operadores lógicos combinan condiciones booleanas.
Son los que permiten expresar lógica compleja en una
sola línea: "si llueve Y no tengo paraguas",
"si es fin de semana O es festivo".
Aparecen en cada if con condiciones múltiples
y en cada filtro de pandas con varias condiciones.
# Tabla de verdad — ejecuta y observa
print("=== AND ===")
print(True and True) # ambos verdaderos → True
print(True and False) # uno falso → False
print(False and True) # False primero → False
print(False and False) # ambos falsos → False
print("=== OR ===")
print(True or True) # alguno verdadero → True
print(True or False) # alguno verdadero → True
print(False or True) # alguno verdadero → True
print(False or False) # ambos falsos → False
print("=== NOT ===")
print(not True) # invierte → False
print(not False) # invierte → True
— Cortocircuito: Python no evalúa más de lo necesario —
Python evalúa las condiciones de izquierda a derecha
y se detiene en cuanto conoce el resultado final.
Con and: si el primer operando es False,
el resultado ya es False — no evalúa el segundo.
Con or: si el primer operando es True,
el resultado ya es True — no evalúa el segundo.
Esto se llama evaluación de cortocircuito
y tiene consecuencias prácticas importantes.
# Cortocircuito con and: si el primero es False, el segundo no se evalúa
def condicion_costosa():
print(" → evaluando condición costosa...")
return True
print("Con False and ...:")
resultado = False and condicion_costosa() # ¿se llama la función?
print(f"Resultado: {resultado}")
print("Con True and ...:")
resultado = True and condicion_costosa() # ¿se llama la función?
print(f"Resultado: {resultado}")
# Uso práctico: evitar errores con cortocircuito
lista = []
# Sin cortocircuito: lista[0] daría IndexError si lista está vacía
# Con cortocircuito: si lista es vacía (falsy), no evalúa lista[0]
if lista and lista[0] > 5:
print("El primer elemento es mayor que 5")
else:
print("Lista vacía o primer elemento <= 5")
— Valores truthy y falsy: todo objeto tiene un valor de verdad —
En Python no necesitas comparar explícitamente con
True o False — cualquier objeto
puede usarse directamente en una condición.
Python lo convierte implícitamente a booleano.
Los valores que se comportan como False
se llaman falsy; el resto son
truthy.
Son falsy: 0, 0.0,
"", [], {},
(), None.
Todo lo demás es truthy.
Esta es una de las características más importantes
para leer código Python real — aparece en todas partes.
# Predice antes de ejecutar: ¿truthy o falsy?
valores = [0, 1, "", "hola", [], [1, 2], None, 0.0, 3.14]
for v in valores:
print(f"{repr(v):12} → {bool(v)}")
# Uso en código real — estas dos formas son equivalentes
nombre = ""
# Forma explícita
if nombre != "":
print(f"Hola, {nombre}")
# Forma pythónica — más común en código real
if nombre:
print(f"Hola, {nombre}")
else:
print("Nombre vacío")
— Booleanos en análisis de datos —
En pandas, los filtros producen Series de booleanos —
una máscara que indica qué filas cumplen la condición.
Entender que un filtro de pandas es simplemente
una lista de True y False
es lo que te permite combinarlos, invertirlos
y entender qué está haciendo el código.
import pandas as pd
df = pd.DataFrame({
"producto": ["A", "B", "C", "D", "E"],
"ventas": [120, 45, 200, 80, 310],
"activo": [True, False, True, True, False]
})
# Un filtro produce una Serie de booleanos
mascara = df["ventas"] > 100
print(type(mascara))
print(mascara)
# Aplicar la máscara para filtrar filas
print(df[mascara])
# Combinar condiciones con & (and) y | (or)
# Nota: en pandas se usa & y | en lugar de and y or
filtro = (df["ventas"] > 100) & (df["activo"] == True)
print(df[filtro])
# Contar cuántas filas cumplen la condición
print(f"Productos con ventas > 100: {mascara.sum()}")
print(f"Porcentaje: {mascara.mean() * 100:.1f}%")
df["ventas"] > 100
devuelve una Serie de booleanos — una por cada fila.
Esa Serie se puede usar directamente para filtrar
el DataFrame, combinarla con otras condiciones
con & y |, invertirla con ~,
o usar sum() y mean()
para contar y calcular porcentajes.
Todo esto funciona porque los booleanos
son subtipos de int.
bool como subtipo
de int. Predice antes:
¿qué devuelve True + True?
¿Y True + 5? ¿Y False * 100?
¿Cuántos aprobados hay en la lista de notas?
condicion_costosa()
cuando el primer operando es False?
¿Y cuando es True?
¿Qué ventaja práctica tiene esto
en un programa real?
bool()
para cada valor de la lista.
¿Cuáles te sorprenden?
¿Por qué bool([]) es False
pero bool([0]) es True?
mascara? ¿Cuántos productos
tienen ventas mayores de 100?
¿Qué devuelve mascara.mean()
y qué representa ese número?
x = 5
y = 10
z = 3
print(x > z and y > x)
print(x > y or z < x)
print(not (x == z))
print(x > z > y) # ¿qué hace esto?
print(bool(x - 5)) # ¿True o False?
True + True → 2
porque True == 1.
True + 5 → 6.
False * 100 → 0
porque False == 0.
En la lista de notas hay 4 aprobados
(notas ≥ 5): 7.0, 8.5, 5.0 y 9.0.
sum(aprobados) cuenta los
True sumándolos como enteros.
Cuando el primer operando es False,
condicion_costosa() no se llama —
no aparece el mensaje en pantalla.
Python sabe que False and X
siempre es False sin necesidad
de evaluar X.
Cuando es True, sí se llama.
if lista and lista[0] > 5
nunca lanza IndexError porque
si lista está vacía
(falsy), Python no evalúa lista[0].
Falsy: 0, "",
[], None, 0.0.
Truthy: 1, "hola",
[1, 2], 3.14.
bool([]) es False
porque la lista está vacía — no tiene elementos.
bool([0]) es True
porque la lista tiene un elemento — aunque
ese elemento sea 0, la lista
en sí no está vacía.
Python evalúa la verdad del contenedor,
no de su contenido.
mascara es una pd.Series
de booleanos. Hay 3 productos con ventas
mayores de 100: A (120), C (200) y E (310).
mascara.mean() devuelve
0.6 — el 60% de los productos
tienen ventas mayores de 100.
Funciona porque mean() trata
los booleanos como 0 y 1,
y la media de una lista de ceros y unos
es la proporción de unos.
x > z and y > x →
5 > 3 and 10 > 5 →
True and True → True.
x > y or z < x →
False or True → True.
not (x == z) →
not False → True.
x > z > y →
5 > 3 > 10 → False:
Python evalúa comparaciones encadenadas
como 5 > 3 and 3 > 10.
bool(x - 5) →
bool(0) → False:
0 es falsy.
x > z > y no significa
(x > z) > y — significa
x > z and z > y.
En este caso 5 > 3 es True
pero 3 > 10 es False,
así que el resultado es False.
Python no convierte tipos automáticamente — salvo
en casos muy concretos como mezclar int
y float en una operación aritmética.
En el resto de casos, si necesitas un tipo distinto
al que tienes, debes convertirlo tú explícitamente.
Esto se llama casting.
En análisis de datos el casting es una operación
cotidiana: los datos casi siempre llegan con el tipo
incorrecto. Números que vienen como strings de un CSV,
fechas como texto, booleanos como 0 y 1.
Saber cuándo convertir y qué puede fallar es
una de las habilidades más prácticas de este curso.
— Las cuatro funciones de conversión básicas —
# int() — convierte a entero
print(int("42")) # str → int
print(int(3.9)) # float → int: ¿redondea o trunca?
print(int(True)) # bool → int
print(int(False)) # bool → int
# float() — convierte a decimal
print(float("3.14")) # str → float
print(float(42)) # int → float
print(float(True)) # bool → float
# str() — convierte a texto
print(str(42)) # int → str
print(str(3.14)) # float → str
print(str(True)) # bool → str
print(str(None)) # None → str: ¿qué devuelve?
# bool() — convierte a booleano
print(bool(0)) # 0 → False
print(bool(42)) # cualquier otro número → True
print(bool("")) # string vacío → False
print(bool("False")) # ¿True o False? Piensa bien
Dos resultados que suelen sorprender:
int(3.9) devuelve 3 —
trunca, no redondea. Si necesitas redondear,
usa round().
Y bool("False") devuelve True
— porque "False" es un string no vacío,
y cualquier string no vacío es truthy.
Para convertir el string "True"/"False"
a booleano correctamente necesitas lógica explícita,
no bool().
— Conversiones que fallan: qué error producen —
No todas las conversiones son posibles. Antes de ejecutar cada línea, predice si fallará y qué tipo de error producirá.
# Predice: ¿cuáles fallan y cuáles no?
conversiones = [
("int('3.14')", lambda: int("3.14")), # ¿funciona?
("int('42')", lambda: int("42")), # ¿funciona?
("int('hola')", lambda: int("hola")), # ¿funciona?
("float('abc')", lambda: float("abc")), # ¿funciona?
("int(None)", lambda: int(None)), # ¿funciona?
("float('1e3')", lambda: float("1e3")), # ¿funciona?
]
for nombre, fn in conversiones:
try:
resultado = fn()
print(f"✓ {nombre:20} → {resultado} ({type(resultado).__name__})")
except Exception as e:
print(f"✗ {nombre:20} → {type(e).__name__}: {e}")
— Conversión implícita: los pocos casos donde Python convierte solo —
Python convierte tipos automáticamente solo en casos muy específicos y predecibles. Conocerlos evita sorpresas al leer código ajeno.
# int + float → float siempre
resultado = 3 + 1.0
print(type(resultado), resultado)
# bool en operaciones aritméticas → int
print(True + 1) # 2
print(False + 1.5) # 1.5
# Python NO convierte automáticamente en estos casos
try:
print("42" + 1) # str + int → TypeError
except TypeError as e:
print(f"TypeError: {e}")
try:
print("3.14" * 2) # str * int → repetición, no multiplicación
except Exception as e:
print(f"Error: {e}")
else:
print("No hubo error — ¿el resultado es el esperado?")
— Caso real: pipeline de limpieza de datos —
Este es el patrón más frecuente en análisis de datos reales: los valores llegan como strings desde un CSV o una API y hay que convertirlos antes de operar. Algunos valores pueden ser incorrectos — hay que manejarlos sin que el programa se detenga.
# Datos típicos de un CSV mal formateado
datos_crudos = [
{"producto": "A", "ventas": "1200", "activo": "1"},
{"producto": "B", "ventas": " 850", "activo": "0"},
{"producto": "C", "ventas": "N/A", "activo": "1"},
{"producto": "D", "ventas": "2100", "activo": "1"},
]
datos_limpios = []
for fila in datos_crudos:
try:
ventas = int(fila["ventas"].strip()) # strip + conversión
activo = bool(int(fila["activo"])) # "1"→1→True, "0"→0→False
datos_limpios.append({
"producto": fila["producto"],
"ventas": ventas,
"activo": activo
})
except ValueError as e:
print(f"Fila ignorada — {fila['producto']}: {e}")
print("\nDatos limpios:")
for fila in datos_limpios:
print(fila)
# Ahora podemos operar correctamente
total = sum(f["ventas"] for f in datos_limpios)
print(f"\nTotal ventas válidas: {total}")
print(f"Productos activos: {sum(f['activo'] for f in datos_limpios)}")
int(3.9)?
¿Redondea o trunca? ¿Y bool("False")?
¿Por qué ese resultado?
"3.14" * 2 no lanza error.
¿Qué devuelve? ¿Es el resultado que esperabas?
¿Cómo lo convertirías para obtener
la multiplicación numérica correcta?
int(3.9) devuelve 3
— trunca hacia cero, no redondea.
int(-3.9) devuelve -3,
no -4. Si necesitas redondear
al entero más cercano usa round(3.9)
que devuelve 4.
bool("False") devuelve True
porque "False" es un string
no vacío — y cualquier string no vacío
es truthy. bool() no interpreta
el contenido del string, solo si está vacío o no.
Para convertir correctamente:
valor = "False"
booleano = valor.strip().lower() == "true"
print(booleano) # False
int("3.14") → falla con
ValueError: no puede convertir
un string con decimal a entero directamente.
Solución: int(float("3.14")).
int("42") → funciona: 42.
int("hola") → falla con
ValueError.
float("abc") → falla con
ValueError.
int(None) → falla con
TypeError: tipo incompatible.
float("1e3") → funciona: 1000.0
— Python entiende notación científica en strings.
"3.14" * 2 devuelve
"3.143.14" — Python repite
el string dos veces porque str * int
es repetición. No hay error pero el resultado
es completamente incorrecto para una multiplicación
numérica. Para obtener 6.28:
print(float("3.14") * 2) # 6.28
"3.14" * 2 no lanza ningún error —
produce un resultado que parece válido
pero es incorrecto. Es exactamente el tipo
de error que no detectas sin verificar el tipo
antes de operar.
Se ignora la fila del producto "C"
porque int("N/A".strip())
lanza un ValueError:
"N/A" no es convertible a entero.
Para guardar las filas con error
en lugar de ignorarlas:
datos_limpios = []
datos_error = []
for fila in datos_crudos:
try:
ventas = int(fila["ventas"].strip())
activo = bool(int(fila["activo"]))
datos_limpios.append({
"producto": fila["producto"],
"ventas": ventas,
"activo": activo
})
except ValueError as e:
datos_error.append({**fila, "error": str(e)})
Tres tipos de tareas: dos vídeos, ejercicios de práctica y ejercicios de profundización. Los ejercicios se hacen en JupyterLab, cada uno en su propia celda. Los de profundización son opcionales — para quien quiera ir más allá.
— Tarea 1: vídeo —
Este vídeo explica con animaciones exactamente el modelo de etiquetas que hemos trabajado hoy — incluyendo la representación interna de los objetos en memoria (PyObject). Es el complemento visual perfecto para consolidar la Parte II.
▶ Ver vídeo — How variables work in PythonMientras lo ves, anota: ¿qué tres cosas del vídeo conectan con lo que hemos visto hoy en clase? ¿Hay algo que te sorprenda o que contradiga lo que creías entender? Lo discutiremos al inicio de la Clase 3.
El vídeo a continuación está mas relacionado al pensamiento crítico: analiza cómo está
cambiando la programación con la llegada
de los asistentes de IA. Presenta distintos niveles de trabajo con LLMs:
desde usar IA como autocompletado hasta delegar tareas completas mediante
especificaciones y revisión de resultados.
Aunque algunas afirmaciones son deliberadamente provocadoras,
el vídeo plantea una cuestión muy relevante para este curso:
¿qué habilidades siguen siendo humanas cuando la IA también escribe código?
Mientras lo ves, piensa en estas preguntas: ¿en qué nivel dirías que trabajas tú actualmente? ¿Qué cosas haces “de verdad” tú y cuáles delegas ya en la IA? ¿Qué significa realmente comprender un programa cuando parte del código ha sido generado automáticamente?
— Tarea 2: ejercicios de práctica —
Crea variables peso = 70 (kg)
y altura = 1.75 (m).
Calcula el IMC con la fórmula
peso / altura²
y muestra el resultado redondeado a 2 decimales
con un f-string descriptivo.
peso = 70
altura = 1.75
imc = peso / (altura ** 2)
print(f"Peso: {peso} kg | Altura: {altura} m | IMC: {imc:.2f}")
** es el operador de potencia.
Los paréntesis son necesarios —
sin ellos peso / altura ** 2
calcularía peso / (altura ** 2)
por la precedencia de operadores,
que en este caso coincide, pero
es mejor ser explícito.
Crea variables precio_original = 49.99
y descuento_porcentaje = 15.
Calcula cuánto dinero ahorras y el precio final.
Muestra todo con f-strings y 2 decimales.
precio_original = 49.99
descuento_porcentaje = 15
descuento_euros = precio_original * (descuento_porcentaje / 100)
precio_final = precio_original - descuento_euros
print(f"Precio original: {precio_original:.2f}€")
print(f"Descuento ({descuento_porcentaje}%): -{descuento_euros:.2f}€")
print(f"Precio final: {precio_final:.2f}€")
Crea variables booleanas:
es_estudiante = True,
tiene_descuento = False,
es_fin_semana = True.
Usa operadores lógicos para decidir si aplicar
descuento — solo si es estudiante
Y además es fin de semana
O tiene descuento.
Antes de ejecutar, predice el resultado.
es_estudiante = True
tiene_descuento = False
es_fin_semana = True
aplicar = es_estudiante and (es_fin_semana or tiene_descuento)
print(f"¿Aplicar descuento? {aplicar}")
Los paréntesis son imprescindibles aquí.
Sin ellos, and tiene mayor
precedencia que or y
el resultado sería distinto.
Crea texto = " Hola Mundo Python ".
Usando métodos de string: elimina los espacios,
convierte a mayúsculas, cuenta las palabras
y verifica si contiene "Python".
Muestra cada resultado con su tipo.
texto = " Hola Mundo Python "
limpio = texto.strip()
mayusculas = limpio.upper()
palabras = limpio.split()
contiene = "Python" in limpio
print(f"Limpio: '{limpio}'")
print(f"Mayúscula: '{mayusculas}'")
print(f"Palabras: {len(palabras)} → {palabras}")
print(f"Python: {contiene} ({type(contiene).__name__})")
— Tarea 3: profundización (opcional) —
Crea a = [1, 2, 3], b = a
y c = [1, 2, 3].
Usa id(), is y ==
para explorar la diferencia entre identidad y igualdad.
Luego haz a.append(4) y observa
qué pasa con b y c.
Predice antes de ejecutar.
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(f"a == b: {a == b}, a is b: {a is b}") # True, True
print(f"a == c: {a == c}, a is c: {a is c}") # True, False
a.append(4)
print(f"\nb: {b}") # cambia — mismo objeto
print(f"c: {c}") # no cambia — objeto distinto
== compara
valores — is compara identidades.
Dos objetos pueden ser iguales (==)
sin ser el mismo objeto (is).
Crea a = [1, 2] y b = a.
Ejecuta a += [3] y observa qué pasa
con el id de a y con b.
Luego repite con a = a + [4].
Compara el comportamiento con números:
x = 5, y = x,
x += 3.
Predice antes de ejecutar cada paso.
# Con listas: += modifica en lugar — mismo id
a = [1, 2]
b = a
print(f"Antes: id(a)={hex(id(a))}, b={b}")
a += [3]
print(f"Después +=: id(a)={hex(id(a))}, b={b}") # b cambia
a = a + [4]
print(f"Después = +: id(a)={hex(id(a))}, b={b}") # b no cambia
# Con números: += siempre crea objeto nuevo
x = 5
y = x
x += 3
print(f"\nx={x}, y={y}") # y no cambia
a += [3] y a = a + [3]
no son equivalentes — el primero modifica
el objeto en su lugar, el segundo crea uno nuevo.
Con números siempre crean objeto nuevo.
Con x = 5 y y = 2,
evalúa paso a paso esta expresión
antes de ejecutarla:
resultado = x + y * 3 ** 2 - x // y + x % y
Descompón en sub-expresiones en el orden en que Python las evalúa y verifica el resultado final.
x, y = 5, 2
# Precedencia: ** → *, //, % → +, -
print(f"3 ** 2 = {3 ** 2}") # 9
print(f"y * 9 = {y * 9}") # 18
print(f"x // y = {x // y}") # 2
print(f"x % y = {x % y}") # 1
print(f"x + 18 - 2 + 1 = {x + 18 - 2 + 1}") # 22
resultado = x + y * 3 ** 2 - x // y + x % y
print(f"\nResultado: {resultado}") # 22
** primero,
luego *, //, %
de izquierda a derecha,
finalmente + y -.
Cuando tengas dudas, usa paréntesis.
Usa sys.getrefcount() para explorar
cuántas referencias apuntan a un objeto en cada momento.
Crea una lista grande, añade referencias con nuevas
etiquetas y elimínalas con del.
Observa cómo varía el contador de referencias.
import sys
datos = list(range(1000))
print(f"Referencias: {sys.getrefcount(datos) - 1}") # 1
copia1 = datos
copia2 = datos
print(f"Referencias: {sys.getrefcount(datos) - 1}") # 3
del copia1
print(f"Referencias: {sys.getrefcount(datos) - 1}") # 2
del copia2
print(f"Referencias: {sys.getrefcount(datos) - 1}") # 1
getrefcount() devuelve
siempre uno más de lo esperado porque
la propia llamada a la función
crea una referencia temporal.
Por eso restamos 1.
Cuando el contador llega a 0,
el objeto es elegible para
el recolector de basura.
— Lectura opcional —
Sección 3.1 de la documentación oficial de Python: Objects, values and types. Es la fuente de todo lo que hemos visto hoy — tres párrafos densos pero con toda la precisión técnica. Léela después de haber hecho los ejercicios, no antes.
→ Documentación oficial — Objects, values and types