Encontrar texto dentro de texto
Buscar un fragmento dentro de una cadena es una de las operaciones más habituales en cualquier programa. Comprobar si un correo contiene una arroba, localizar una palabra clave en una descripción, extraer el dominio de una URL, filtrar líneas de un registro que mencionan un error o validar que un nombre de archivo termina en una extensión concreta son tareas que aparecen constantemente.
Python ofrece varias herramientas para ello, y cada una responde a una pregunta ligeramente distinta. ¿Está el texto presente o no? ¿En qué posición aparece por primera vez? ¿Y la última? ¿Cuántas veces? ¿Quiero que falle si no está? ¿Necesito ignorar mayúsculas o buscar patrones más complejos que un texto fijo?
En esta guía veremos el método find en detalle y lo compararemos con index, rfind, count, el operador in, startswith, endswith, partition y el módulo re de expresiones regulares. También repasaremos cómo buscar sin distinguir mayúsculas, cómo encontrar todas las apariciones, qué ocurre con Unicode y cuáles son los errores más frecuentes. Todos los ejemplos funcionan con Python 3.15, la versión estable más reciente, y con cualquier versión de Python 3.
Cadenas e índices en Python
Antes de buscar conviene recordar cómo se numeran las posiciones de una cadena. Cada carácter tiene un índice, y el primero es el 0:
Texto: E l c a f é
Índice: 0 1 2 3 4 5 6
Una búsqueda devuelve, normalmente, el índice del primer carácter de la coincidencia. Las cadenas en Python son inmutables: ningún método de búsqueda modifica la cadena original, todos devuelven un nuevo valor.
Usaremos esta frase en la mayoría de ejemplos:
frase = "El café de Colombia es suave; el café de Etiopía es floral."
El método find
find busca una subcadena y devuelve el índice de su primera aparición. Si no la encuentra, devuelve -1:
print(frase.find("café"))
print(frase.find("té"))
3
-1
La primera aparición de café empieza en el índice 3, y té no está en la frase, por lo que el resultado es -1.
La búsqueda distingue entre mayúsculas y minúsculas: frase.find("Café") también devolvería -1, porque en la frase la palabra está en minúsculas.
La firma completa del método admite dos parámetros opcionales que delimitan la zona de búsqueda:
cadena.find(subcadena, inicio, fin)
inicio indica desde qué índice empezar a buscar, y fin, hasta qué índice, sin incluirlo. Los índices devueltos siguen siendo relativos a la cadena completa, no a la porción buscada:
print(frase.find("café", 5))
print(frase.find("café", 10, 20))
33
-1
Al empezar en el índice 5, la primera coincidencia ya ha quedado atrás y find encuentra la segunda, en el 33. Entre los índices 10 y 20 no hay ninguna.
Este parámetro de inicio permite recorrer todas las apariciones con un bucle sencillo:
def todas_las_posiciones(texto, buscado):
posiciones = []
indice = texto.find(buscado)
while indice != -1:
posiciones.append(indice)
indice = texto.find(buscado, indice + 1)
return posiciones
print(todas_las_posiciones(frase, "café"))
[3, 33]
La trampa del valor -1
El valor de retorno -1 es la fuente del error más común con find. En Python, -1 es un índice válido que señala el último carácter de la cadena. Si usas el resultado de find sin comprobarlo, el programa no fallará, pero hará algo incorrecto:
correo = "usuario.ejemplo.com" # falta la arroba
arroba = correo.find("@")
dominio = correo[arroba + 1:]
print(dominio)
usuario.ejemplo.com
Como arroba vale -1, arroba + 1 vale 0 y el corte devuelve la cadena entera, sin ningún aviso. Además, -1 es un valor verdadero en Python, así que tampoco sirve escribir if correo.find("@"):, porque también es falso cuando la arroba está en la posición 0.
La comprobación correcta compara explícitamente con -1:
arroba = correo.find("@")
if arroba == -1:
print("Correo no válido")
else:
print(correo[arroba + 1:])
El método index
index funciona igual que find, con los mismos parámetros, pero cuando no encuentra la subcadena lanza una excepción ValueError en lugar de devolver -1:
print(frase.index("Etiopía"))
frase.index("té")
41
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ValueError: substring not found
La elección entre find e index en Python depende de lo que signifique la ausencia del texto. Si es una situación normal que tu código debe manejar, find es más cómodo. Si su ausencia indica un error, porque el texto siempre debería estar, index es preferible: falla de forma ruidosa en lugar de producir resultados incorrectos en silencio.
def extraer_dominio(correo):
try:
return correo[correo.index("@") + 1:]
except ValueError:
raise ValueError(f"'{correo}' no contiene una arroba") from None
Otra diferencia práctica: las listas y las tuplas también tienen un método index, pero no tienen find. Este método es exclusivo de las cadenas y de los tipos bytes y bytearray.
Comprobar si existe con el operador in
Muy a menudo no interesa la posición, solo saber si el texto está o no. Para eso, la forma más clara y recomendada es el operador in:
print("suave" in frase)
print("amargo" in frase)
print("amargo" not in frase)
True
False
True
El operador in en Python devuelve directamente un booleano, se lee casi como lenguaje natural y es tan rápido como find, porque usa el mismo algoritmo internamente. Usar find para esta tarea es un antipatrón común:
# Menos claro
if frase.find("suave") != -1:
...
# Recomendado
if "suave" in frase:
...
Para comprobar varias palabras a la vez, se combina con any o all:
alergenos = ["cacahuete", "gluten", "lactosa"]
descripcion = "Galletas de avena con trozos de cacahuete"
if any(alergeno in descripcion for alergeno in alergenos):
print("Contiene alérgenos")
Contiene alérgenos
Buscar desde el final con rfind y rindex
rfind y rindex buscan la última aparición en lugar de la primera. Devuelven igualmente el índice del inicio de la coincidencia, contado desde el principio de la cadena:
print(frase.rfind("café"))
print(frase.rfind("es"))
33
49
Son especialmente útiles para separar la última parte de una ruta o un nombre de archivo:
archivo = "informe.ventas.2026.xlsx"
punto = archivo.rfind(".")
print(archivo[:punto], archivo[punto + 1:])
informe.ventas.2026 xlsx
Para rutas y extensiones reales, el módulo pathlib es más robusto, porque tiene en cuenta los separadores de cada sistema operativo:
from pathlib import Path
ruta = Path("datos/informe.ventas.2026.xlsx")
print(ruta.stem, ruta.suffix)
informe.ventas.2026 .xlsx
Contar apariciones con count
count devuelve cuántas veces aparece una subcadena, y admite los mismos parámetros de inicio y fin:
print(frase.count("café"))
print(frase.count("es"))
print(frase.count("té"))
2
2
0
Hay un detalle importante: count cuenta apariciones que no se solapan. En la cadena "aaaa", la subcadena "aa" aparece dos veces según count, aunque empieza en tres posiciones distintas:
print("aaaa".count("aa"))
2
Si necesitas contar coincidencias solapadas, por ejemplo en análisis de secuencias genéticas, tendrás que usar un bucle con find avanzando de uno en uno, como vimos antes, o una expresión regular con búsqueda anticipada.
Comprobar el principio y el final
Cuando lo que importa es si una cadena empieza o termina con un texto, startswith y endswith son más claros y eficientes que find, porque solo comprueban un extremo:
print(frase.startswith("El"))
print(frase.endswith("."))
True
True
Ambos aceptan una tupla con varias opciones, algo muy útil para validar extensiones de archivo:
extensiones_imagen = (".jpg", ".jpeg", ".png", ".webp")
nombre = "foto_vacaciones.webp"
if nombre.lower().endswith(extensiones_imagen):
print("Es una imagen")
Es una imagen
Fíjate en que debe ser una tupla: si pasas una lista, Python lanzará un TypeError.
Desde Python 3.9 existen además removeprefix y removesuffix, que eliminan un prefijo o sufijo si está presente, sustituyendo a muchos usos de find combinados con cortes:
url = "https://www.tienda.example"
print(url.removeprefix("https://"))
www.tienda.example
Dividir por la primera coincidencia con partition
Muchas veces se busca una subcadena solo para partir el texto en dos. El método partition hace exactamente eso: busca la primera aparición del separador y devuelve una tupla con lo que hay antes, el separador y lo que hay después:
cabecera = "Content-Type: application/json; charset=utf-8"
clave, separador, valor = cabecera.partition(": ")
print(clave)
print(valor)
Content-Type
application/json; charset=utf-8
Si el separador no aparece, devuelve la cadena completa seguida de dos cadenas vacías, sin lanzar errores. rpartition hace lo mismo con la última aparición. Comparado con find y cortes manuales, es más legible y evita el error del -1.
Buscar sin distinguir mayúsculas
Todos los métodos anteriores distinguen entre mayúsculas y minúsculas. Para una búsqueda sin distinguir mayúsculas, lo habitual es convertir ambas cadenas al mismo formato antes de comparar:
titulo = "Guía Completa de PYTHON para Principiantes"
consulta = "python"
print(consulta in titulo)
print(consulta.lower() in titulo.lower())
print(titulo.lower().find(consulta.lower()))
False
True
17
Para texto en idiomas distintos del inglés, casefold es más adecuado que lower, porque aplica reglas de plegado de mayúsculas más completas. El ejemplo clásico es la ß alemana:
print("Straße".lower() == "strasse")
print("Straße".casefold() == "strasse")
False
True
Como la conversión no cambia la longitud del texto en la mayoría de casos, el índice obtenido sobre la versión en minúsculas suele servir para la cadena original. Pero hay excepciones, como la propia ß, que se convierte en dos caracteres con casefold. Si necesitas posiciones exactas, usa una expresión regular con la opción de ignorar mayúsculas.
Búsquedas avanzadas con expresiones regulares
Cuando el texto que buscas no es fijo, sino un patrón, entra en juego el módulo re. Permite buscar números, fechas, direcciones de correo o palabras completas.
re.search devuelve un objeto con información de la primera coincidencia, o None si no hay ninguna:
import re
pedido = "Pedido 1042 enviado el 2026-10-17 a Zaragoza"
coincidencia = re.search(r"\d{4}-\d{2}-\d{2}", pedido)
if coincidencia:
print(coincidencia.group(), coincidencia.start(), coincidencia.end())
2026-10-17 23 33
Para encontrar todas las apariciones de una subcadena o patrón con sus posiciones, re.finditer es la herramienta más cómoda:
print([m.start() for m in re.finditer("café", frase)])
[3, 33]
Si el texto que buscas viene del usuario y puede contener caracteres especiales como puntos o paréntesis, escápalo con re.escape para que se busque literalmente:
consulta = "precio (IVA incl.)"
patron = re.compile(re.escape(consulta), re.IGNORECASE)
Las expresiones regulares también resuelven la búsqueda de palabras completas. Con find, buscar es en nuestra frase encontraría también la sílaba de palabras como está o mesa. Con el delimitador \b, solo se encuentran palabras completas:
print(re.search(r"\bes\b", frase).start())
20
Y las búsquedas solapadas, que count no detecta, se resuelven con una búsqueda anticipada:
print([m.start() for m in re.finditer(r"(?=aa)", "aaaa")])
[0, 1, 2]
Las expresiones regulares son potentes, pero más lentas y difíciles de leer. Úsalas cuando los métodos de cadena no basten.
Unicode y caracteres especiales
Las cadenas de Python son Unicode, de modo que las búsquedas funcionan con cualquier idioma. Pero hay un detalle que puede causar resultados sorprendentes: algunos caracteres se pueden representar de varias formas. La é puede ser un único carácter o una e seguida de un acento combinado:
compuesta = "cafe\u0301"
print(compuesta)
print(compuesta == "café")
café
False
Aunque se ven iguales, son cadenas distintas, y una búsqueda de "café" no encontraría la segunda. Este problema aparece con texto procedente de distintos sistemas operativos, formularios web o documentos copiados. La solución es normalizar ambas cadenas antes de buscar:
import unicodedata
def normalizar(texto):
return unicodedata.normalize("NFC", texto)
print(normalizar(compuesta) == "café")
True
Para búsquedas que ignoren también las tildes, como hace un buscador, se puede descomponer el texto con la forma NFD y eliminar las marcas diacríticas:
def sin_tildes(texto):
descompuesto = unicodedata.normalize("NFD", texto)
return "".join(c for c in descompuesto if unicodedata.category(c) != "Mn")
print(sin_tildes("Etiopía").lower() == "etiopia")
True
Casos prácticos
Veamos algunos problemas reales que se resuelven combinando las herramientas anteriores.
El primero es filtrar las líneas de un archivo de registro que contienen errores. Leer el archivo línea a línea evita cargarlo entero en memoria, y el operador in hace la comprobación:
from pathlib import Path
def lineas_con_error(ruta, palabras=("ERROR", "CRITICAL")):
with Path(ruta).open(encoding="utf-8") as archivo:
for numero, linea in enumerate(archivo, start=1):
if any(palabra in linea for palabra in palabras):
yield numero, linea.rstrip()
for numero, linea in lineas_con_error("app.log"):
print(f"{numero}: {linea}")
42: 2026-10-17 14:05:12 ERROR No se pudo conectar con la base de datos
97: 2026-10-17 14:09:48 CRITICAL Disco lleno en /var/lib/datos
El segundo es extraer el texto que hay entre dos delimitadores, como el contenido de una etiqueta o de unos corchetes. Combinando find con el parámetro de inicio, se localiza el cierre a partir de la apertura:
def entre(texto, apertura, cierre):
inicio = texto.find(apertura)
if inicio == -1:
return None
inicio += len(apertura)
fin = texto.find(cierre, inicio)
if fin == -1:
return None
return texto[inicio:fin]
mensaje = "Tu pedido [PED-1042] está en camino"
print(entre(mensaje, "[", "]"))
PED-1042
Fíjate en que hay que sumar la longitud del delimitador de apertura, porque find devuelve el índice de su primer carácter, no el del texto que le sigue.
El tercero es resaltar en un texto todas las apariciones de un término de búsqueda, ignorando mayúsculas, como haría un buscador en una página de resultados. Una expresión regular con re.sub conserva el texto original de cada coincidencia:
import re
def resaltar(texto, termino):
patron = re.compile(re.escape(termino), re.IGNORECASE)
return patron.sub(lambda m: f"<mark>{m.group()}</mark>", texto)
print(resaltar("Python es genial. Aprende python hoy.", "python"))
<mark>Python</mark> es genial. Aprende <mark>python</mark> hoy.
El cuarto es validar de forma rápida que una URL apunta a un dominio permitido. Aquí la tentación es usar in, pero sería inseguro: "tienda.example" in url también aceptaría https://tienda.example.atacante.com. Es más seguro analizar la URL y comparar el nombre de host completo:
from urllib.parse import urlparse
PERMITIDOS = {"tienda.example", "www.tienda.example"}
def es_url_permitida(url):
return urlparse(url).hostname in PERMITIDOS
print(es_url_permitida("https://www.tienda.example/carrito"))
print(es_url_permitida("https://tienda.example.atacante.com/"))
True
False
Este último ejemplo ilustra una lección importante: buscar subcadenas es perfecto para texto libre, pero para datos con estructura, como URLs, rutas, JSON o correos, es preferible usar el analizador específico de ese formato.
Rendimiento
Los métodos de búsqueda de las cadenas están implementados en C y usan algoritmos optimizados, por lo que son muy rápidos incluso con textos grandes. Algunas pautas para sacarles el máximo partido:
- Usa
inpara comprobar presencia,findoindexpara posiciones ycountpara frecuencias; todos comparten el mismo motor eficiente. - Prefiere los métodos de cadena a las expresiones regulares cuando el texto buscado sea fijo.
- Si vas a usar la misma expresión regular muchas veces, compílala una vez con
re.compile. - Si buscas muchas palabras distintas en el mismo texto, considera convertir el texto en un conjunto de palabras, cuya comprobación de pertenencia es casi instantánea.
- Para búsquedas en grandes volúmenes de documentos, como un buscador interno, las herramientas de búsqueda de texto completo de las bases de datos o motores como Elasticsearch son más adecuados que recorrer cadenas en Python.
palabras = set(frase.lower().replace(";", "").replace(".", "").split())
print("floral" in palabras)
True
Buscar en listas de cadenas
Con frecuencia la búsqueda no se hace en una sola cadena, sino en una colección: los títulos de un catálogo, los nombres de una lista de contactos o las líneas de un documento. Las comprensiones de listas y las funciones filter y next combinan bien con los métodos que hemos visto.
Para obtener todos los elementos que contienen un término:
productos = ["Café molido", "Cafetera italiana", "Té verde", "Taza de café"]
termino = "caf"
coincidencias = [p for p in productos if termino in p.lower()]
print(coincidencias)
['Café molido', 'Cafetera italiana', 'Taza de café']
Y para obtener solo el primero, sin recorrer toda la lista, next con un valor por defecto evita la excepción cuando no hay ninguno:
primero = next((p for p in productos if p.lower().startswith("té")), None)
print(primero)
Té verde
Si además necesitas la posición de cada coincidencia dentro de la lista, enumerate proporciona el índice junto al elemento. Estos patrones son la base de funciones de autocompletado y de filtros en interfaces de usuario.
Errores comunes
El primero es usar el resultado de find sin comprobar si es -1, lo que produce cortes incorrectos en silencio.
El segundo es comprobar el resultado de find como booleano con if texto.find(x):, que falla cuando la coincidencia está en el índice 0 y da un falso positivo cuando no hay coincidencia.
El tercero es olvidar que la búsqueda distingue mayúsculas y minúsculas.
El cuarto es buscar una palabra y encontrarla dentro de otra, como es dentro de mesa. Usa expresiones regulares con \b cuando necesites palabras completas.
El quinto es llamar a find sobre una lista. Las listas no tienen ese método:
AttributeError: 'list' object has no attribute 'find'
Usa in o el método index de las listas.
El sexto es mezclar str y bytes. Buscar una cadena dentro de un objeto bytes produce un error:
TypeError: argument should be integer or bytes-like object, not 'str'
Decodifica primero los bytes con .decode("utf-8") o busca un literal de bytes como b"texto".
Conclusiones
Python ofrece un conjunto completo de herramientas para buscar texto dentro de cadenas, y elegir la adecuada hace el código más claro y robusto. El método find devuelve la posición de la primera aparición o -1 si no la hay, y admite parámetros para limitar la zona de búsqueda. index hace lo mismo, pero lanza una excepción cuando el texto no está, lo que es preferible cuando su ausencia indica un error.
Para saber simplemente si un texto está presente, el operador in es la opción más legible. rfind y rindex buscan desde el final, count cuenta las apariciones sin solapamiento, startswith y endswith comprueban los extremos y partition divide la cadena por la primera coincidencia de forma segura.
Para búsquedas que ignoren mayúsculas, casefold o lower sobre ambas cadenas resuelven la mayoría de casos, y para patrones, palabras completas o todas las coincidencias con sus posiciones, el módulo re con search y finditer es la herramienta indicada. Normalizar el texto Unicode evita sorpresas con caracteres acentuados.
Con estos métodos y la precaución de comprobar siempre el valor -1 de find, podrás resolver cualquier búsqueda de texto en Python de forma clara, eficiente y sin errores silenciosos.
