¿Cuál es la diferencia entre un package y un módulo? ¿cómo funciona el script __init__.py
? ¿qué es el PYTHONPATH?
¡Vamos a por ello! Lo haremos a través de un ejemplo, que es como mejor se entienden las cosas.
Aquí tienes el repositorio de Github donde puedes bichear y encontrar todo lo que leerás a continuación.
Dependencias entre módulos en Python
Antes de entrar en materia, comentarte la diferencia entre módulo y paquete en Python. Es sencillo.
- Módulo : Un script de Python por sí mismo.
- Paquete: Un conjunto de módulos.
Venga, ¡ahora sí! Prepara tu IDE favorito que vamos al tajo.
Vamos a crear una estructura de ficheros como la siguiente:

Y vamos a crear algo de funcionalidad en esos dos ficheros Python module_a.py
y module_b.py
.
En el fichero module_a.py
vamos a añadir el siguiente código:
# module_a.py
A_CONSTANT = 'I am a string constant within the python module a'
def multiply(number: int, times: int):
print(times, " times ", number, " is ", times * number)
Tenemos la constante A_CONSTANT
, que no es más que un String determinado, y la función duplicate
, que va a imprimir por pantalla el valor del múltiplo de los dos números que llegan como parámetro.
Y en el fichero module_b.py
vamos a ver cómo hacer uso de la funcionalidad que nos ofrece module_a.py.
# module_b.py
import module_a
print("The imported string constant is: ", module_a.A_CONSTANT)
module_a.multiply(2, 5)
Desde este script module_b.py
se imprime el valor de la constante de module_a.py
y se llama a la función con los parámetros 2 y 5.
Al ejecutar el script module_b.py
obtenemos la siguiente salida:

Para hacer accesible un módulo desde otro, sencillamente hay que hacer un import indicando el nombre del módulo en cuestión.
Una vez hecho, con ese mismo nombre se puede acceder a todo que el módulo ofrece mediante un punto >> nombre_modulo.funcionalidad.
Así mismo, si por cualquier motivo no nos es cómodo acceder de ese modo, a la hora de hacer el import podemos indicar el alias que queremos usar para hacerlo:
# module_b.py
import module_a as a
print("The imported string constant is: ", a.A_CONSTANT)
a.multiply(2, 5)
De este modo, usando simplemente el alias indicado (en este caso a), se puede acceder a la funcionalidad del módulo.
Qué ocurre realmente cuando se escribe un comando import en Python?
El intérprete de Python intenta buscar el directorio que contiene el módulo que estamos tratando de importar en sys.path
, que no es más que una array con una lista de directorios en los que Python buscará módulos y paquetes cuando termine con los módulos cacheados y las librerías estándar.
Nada mejor para ver de qué estamos hablando que probándolo nosotros mismos.
¡Vamos al lío!
Si modificamos el script del modulo_b.py
para incluir las líneas para imprimir el valor de sys.path
:
# module_b.py
import sys
print(sys.path)
import a
print("The imported string constant is: ", a.A_CONSTANT)
a.multiply(2, 5)
Y lo ejecutamos, obtenemos la siguiente salida cuando se imprime el sys.path. Date cuenta que en mi caso estoy usando Intellij como IDE y tengo un entorno virtual de Python con pyenv. Esta salida puede variar en tu caso.
['{YOUR_WORKSPACE}/imports_init_path/scripts', '{YOUR_WORKSPACE}/imports_init_path', '{YOUR_USER_PATH}/Library/Application Support/JetBrains/IntelliJIdea2021.2/plugins/python/helpers/pycharm_display', '{YOUR_USER_PATH}/.pyenv/versions/3.10.2/lib/python310.zip', '{YOUR_USER_PATH}/.pyenv/versions/3.10.2/lib/python3.10', '{YOUR_USER_PATH}/.pyenv/versions/3.10.2/lib/python3.10/lib-dynload', '{YOUR_USER_PATH}/.pyenv/versions/3.10.2/lib/python3.10/site-packages', '{YOUR_USER_PATH}/Library/Application Support/JetBrains/IntelliJIdea2021.2/plugins/python/helpers/pycharm_matplotlib_backend']
Como ves, en primera posición del array del sys.path tenemos el directorio que creamos scripts. Esto no es casualidad, es que la salida de sys.path
siempre tendrá en la posición 0 el directorio del script que se está ejecutando.
Esta es la principal razón por la que cuando ambos scripts se encuentran en el mismo directorio.
Importar únicamente ciertos elementos de un módulo de Python
En nuestro ejemplo hay muy pocas funcionalidades, pero puede que te encuentres con un módulo que tiene muchas funcionalidades y tú no necesites más que algunas de ellas.
¿Qué ocurre cuando se hace un import?
Cuando se hace el import del módulo como hemos visto hasta ahora, se importa el módulo al completo.
Demostrarlo es sencillo.
Vamos a hacer una llamada desde el mismo módulo b a la función multiply.
Así quedaría el script:
# module_a.py
A_CONSTANT = 'I am a string constant within the python module a'
def multiply(number: int, times: int):
print(times, " times ", number, " is ", times * number)
multiply(10, 10)
Y si ahora ejecutamos nuestro script module_a.py
, veremos que en la salida tenemos dos veces impreso lo que imprime la función multiply, una con los valores 10, 10 y otra con los valores 5, 2.

Workaround para evitar ejecuciones cuando un módulo es importado
El código dentro de la sentencia if__name__ == '__main__'
no se ejecutará al ser importado. Sí que será ejecutado en cambio cuando el módulo es ejecutado él mismo.
# module_a.py
A_CONSTANT = 'I am a string constant within the python module a'
def multiply(number: int, times: int):
print(times, " times ", number, " is ", times * number)
if __name__ == '__main__':
multiply(10, 10)
Si ejecutamos ahora el module_b.py
, la salida que obtenemos es la original.

Y si ejecutamos el module_a.py:

Importar una función en específico
Tenemos que indicar desde qué módulo queremos importar qué función.
# module_b.py
from module_a import multiply
multiply(2, 5)
De esta forma, si ejecutamos el module_b.py
obtenemos la siguiente salida.

De esta manera evitamos tener que hacer uso del carácter punto para especificar, y simplemente podemos llamar a la función en sí sin mayor problema.
Si quisiéramos añadir más de una funcionalidad de un módulo, podemos hacerlo separando por comas.
from module_a import multiply, A_CONSTANT
Así mismo, si quieres importar todas las funciones y objetos de un módulo, también puedes hacerlo así
from module_a import *
Si has comprendido lo comentado hasta ahora, te preguntarás qué diferencia hay entre from module_a import *
y un import module_a
.
La respuesta a fin de funcionamiento es ninguna. Es considerado una mala práctica porque impacta negativamente en la legibilidad del código.
PYTHONPATH
En muchas ocasiones hay ciertas utilidades que pueden ser comunes y que queremos tener como tal en un paquete de utils, common o similar.
Como hasta ahora, vamos a seguir aprendiendo manchándonos las manos. Así que ¡coge de nuevo tu IDE, que seguimos!
Vamos a crear una carpeta de utilidades utils en nuestro proyecto, otra de cosas comunes common, y vamos a crear otro módulo desde donde vamos a hacer uso de ellos.
En la carpeta de utils vamos a tener un módulo module_string.py
y otro module_number.py
. En la carpeta common vamos a tener un módulo de constantes constants.py
y por último nuestro nuevo módulo module_c.py
desde donde usaremos todo esto.

Bien, pues vamos a usar desde nuestro module_c.py
el modulo de utils string.
# module_c.py
import utils.module_string
no_capitalized = "bla bla bla"
capitalized = utils.string.capitalize(no_capitalized)
print(capitalized)
Al ejecutar este script tenemos la siguiente salida:

Si en lugar de hacer el
import utils.module_string
Hubiéramos hecho
import string
Habríamos obtenido ModuleNotFoundError: No module named ‘module_string’
.

¿Por qué? porque el sys.path
que vimos anteriormente no contiene el directorio utils, que se necesita para encontrar el módulo string.
¿Cómo añadir un directorio en el sys.path?
Existen dos formas de hacer esto.
Usando sys.path.append
El array sys.path
no es más que eso, un array. Podemos hacer uso de la función append
para añadir al array el path que nos interese.
En este caso queremos incluir el path donde se encuentran las utilidades. En mi caso es /Users/vnk537/python/imports_init_path/utils
.
Para lograrlo, podemos modificar el script module_c.py
dejándolo como sigue:
import os
import sys
fpath = os.path.join(os.path.dirname(__file__), 'utils')
sys.path.append(fpath)
print(sys.path)
import module_string
no_capitalized = "bla bla bla"
capitalized = module_string.capitalize(no_capitalized)
print(capitalized)
Lo que estamos haciendo con esas líneas añadidas es ni más ni menos que incluir el path que necesitamos en el array sys.path
.
Sabemos que si concatenamos el path del script module_c.py
(os.path.dirname(__file__)
) con el literal utils, tendremos la ruta que buscamos.
Si ejecutamos el script module_c.py
ahora, tendremos la siguiente salida:

Consideraciones:
- Sólo después de añadir al array
sys.path
el path es cuando podemos importar el módulo. os.path.dirname(__file__)
devuelve la ruta absoluta del directorio de trabajo del script desde donde se ejecuta.
Usando la variable de entorno PYTHONPATH
PYTHONPATH
es una variable de entorno que puedes configurar para añadir directorios adicionales donde Python buscará módulos y paquetes.
Como cualquier otra variable de entorno, antes de modificarla hay que considerar los valores que ésta pueda ya tener.
Te aconsejo verificar el valor actual de la variable en tu sistema.

En mi caso está vacía, pero aun así, la buena práctica para añadir valores es tener en cuenta su valor anterior.
Estando en el directorio raíz del proyecto, donde se encuentra nuestra carpeta utils, ejecutamos el siguiente comando:
export PYTHONPATH=$PYTHONPATH:$(pwd)/utils

Una vez hecho esto, ya no necesitamos gestionar el array sys.path
, por lo que dejamos el script module_c.py
como originalmente:
# module_c.py
import module_string
no_capitalized = "bla bla bla"
capitalized = module_string.capitalize(no_capitalized)
print(capitalized)
Y al ejecutarlo obtenemos la siguiente salida:

Consideraciones:
- Cuando cierres python, la lista será revertida a sus valores por defecto. Si quieres añadir de forma permanente a la variable
PYTHONPATH
el directorio, puedes añadir el comando export para incluirlo en tu~/.bashrc
.
Ahora que Python ya sabe que utils forma parte de nuestro path, puedes elegir un método u otro para usar las dependencias.
¡Vamos a maquear nuestro script module_c.py
!
# module_c.py
import module_string
import module_number
no_capitalized = "bla bla bla"
capitalized = module_string.capitalize(no_capitalized)
print(capitalized)
to_evaluate = "123d123"
print("Is ", to_evaluate, " numeric? >> ", module_string.is_numeric(to_evaluate))
with_upper_case = "I dO NoT thinK THis WiLl WorK..."
in_lower_case = module_string.to_lower_case(with_upper_case)
print(in_lower_case)
rest = module_number.module(23, 7)
print("The rest is: ", rest)
Y al ejecutarse así, obtendríamos la siguiente respuesta:

Si te das cuenta, estamos añadiendo todos los módulos que la carpeta utils contiene.
Pero…
¿No estaría aun mejor si pudiéramos hacer algo como import utils
en lugar de tener que hacerlo módulo a módulo? Más cómodo y limpio, ¿no? Pues… ¡sigue leyendo!
¿Qué es __init__.py
y cuándo se necesita de su uso?
El fichero __init__.py
se utiliza para convertir un directorio en un paquete de Python propiamente dicho.
Cuando el intérprete de Python se encuentra con un fichero así, sabe que todo lo que está en ese directorio es un paquete como tal.
Vamos a seguir con nuestra tónica de aprender manchándonos las manos.
Antes de nada, si en los pasos anteriores habíamos modificado la variable de entorno PYTHONPATH
, haz rollback y déjala como estaba antes de que la tocaras.
Vamos a crear un nuevo módulo module_d.py
. De momento sólo vamos a hacer el import de utils y ver qué pasa.
# module_d.py
import utils
Si ejecutamos como tal el módulo, no hay ningún problema y el proceso termina con salida 0.

Todo normal aparentemente. Ahora vamos a tratar de acceder a nuestra función.
is_numeric
que se encuentra en module_string.py
.
Así quedaría nuestro nuevo script
# module_d.py
import utils
to_evaluate = "123d123"
print("Is ", to_evaluate, " numeric? >> ", utils.is_numeric(to_evaluate))
Y al ejecutarlo, esto es lo que obtenemos

El intérprete de Python nos dice que no hay un atributo is_numeric
en el módulo utils.
Y es que sencillamente el intérprete no sabe que utils es un paquete. Como ves, lo trata como si fuera un módulo.
Ahora que ya sabemos para qué sirve el fichero __init__.py
¡vamos a ponerlo en práctica!
Vamos a crear el fichero en el directorio utils, para que el intérprete trate el directorio como un paquete de Python.

Dentro de este fichero __init__.py
, podemos importar todos los módulos que sean necesarios para nuestro proyecto.
Cómo importar desde __init__.py
La intuición en este mundo de los unos y ceros es a veces mala consejera…
Una manera más que razonable de hacer los import en nuestro __init__.py
sería la siguiente:
# utils/__init__.py
from module_string import is_numeric
Tanto es de coherente, que si al código anterior añadimos esta línea
print(is_numeric("17923"))
Y ejecutamos el fichero __init__.py
, obtenemos una salida no sólo coherente, sino correcta y esperada.

Vamos a eliminar esa última línea del __init__.py
, y desde nuestro module_d.py
vamos a ver qué pasa ahora que el intérprete ya sabe que la carpeta utils es un paquete, y que dentro tiene módulos.
Al ejecutar module_d.py
esto es lo que muestra la consola:

Pero… espera un momento Python… ¿me estás vacilando? Te compro que esa sea la única frase que pase por tu cabeza en este momento.
Sin embargo, como casi siempre en este mundillo de las máquinas, ellas tienen la razón.
Y es que aunque aparentemente los import desde el __init__.py
eran más que coherentes, estamos haciendo la llamada desde el fichero module_d.py
, así que el array sys.path
únicamente tendrá su directorio para buscar los imports.
Por tanto, cuando el intérprete encuentra el import utils
, aunque esta vez sí que lo ve como un paquete y se ejecuta el fichero __init__.py
, cuando lo hace, el array sys.path
no se actualiza automáticamente y el intérprete no tiene manera de saber dónde encontrar el módulo module_string.py
.
Debemos indicarle al intérprete dónde se encuentra el directorio utils.
Para hacerlo, podemos o:
- Hacer uso de la variable de entorno
PYTHONPATH
(lo vimos arriba). - Usar import relativos (no recomendado)
- Usar import absolutos (mejor elección).
Import relativos
# utils/__init__.py
from .module_string import is_numeric
print(is_numeric("17923"))
Ejecutamos y…

Import absolutos
# utils/__init__.py
from utils.module_string import is_numeric
Y la salida…

Todo en orden.
Gracias a esta forma de importar el paquete a través de nuestro __init__.py
definido, el código queda mucho más limpio y en nuestro script module_d.py
podemos hacer uso de toda la funcionalidad que necesitemos de dentro del paquete con un código como este:
# utils/__init__.py
from utils.module_string import is_numeric
from utils.module_string import capitalize
from utils.module_number import module
# module_d.py
import utils
no_capitalized = "bla bla bla"
capitalized = utils.capitalize(no_capitalized)
print(capitalized)
to_evaluate = "123d123"
print("Is ", to_evaluate, " numeric? >> ", utils.is_numeric(to_evaluate))
rest = utils.module(23, 7)
print("The rest is: ", rest)
Una de las cosas más bonitas de esta forma de hacer así las cosas es que el paquete utils puede ser importado desde cualquier sitio y ser usado de una forma prácticamente inmediata.
Para que te lo creas, como siempre, vamos a verlo con un ejemplo.
Creamos el script module_e.py
, dentro de nuestra carpeta scripts, para usar desde ahí el paquete utils.
Este es el contenido de nuestro nuevo script:
# scripts/module_e.py
import os
import sys
PROJECT_ROOT = os.path.abspath(os.path.join(
os.path.dirname(__file__),
os.pardir)
)
sys.path.append(PROJECT_ROOT)
import utils
print(utils.capitalize("this is all in lower case."))
Si ejecutamos …

¡Final feliz! 🎉
A estas alturas supongo que no necesitaras que te explique por qué el script está funcionando, pero como soy algo pesado, déjame hacerlo una última vez.
Antes de importar el paquete utils, debemos asegurarnos de que el directorio padre de utils (la raíz en este caso), sea accesible para el intérprete de Python.
Sería imprudente suponer que sucederá de forma predeterminada. Ahora estamos un nivel dentro del directorio raíz del proyecto (estamos ejecutando el script desde la carpeta scripts), sys.path
tendrá /Users/vnk537/python/imports_init_path/scripts
en el índice 0.
Ese es el motivo por el que tenemos que añadir al array el valor del path padre. En nuestro caso /Users/vnk537/python/imports_init_path.
Una vez lo conoce ya has visto lo trivial que es usar el paquete utils desde ahí. Tan sencillo como hacer su import y… ¡usarlo!
Conclusión
Este tipo de errores en python pueden ser un rompe cabezas, sobre todo si estás empezando a lidiar con ello y nunca te has parado a ver cómo funciona de verdad el intérprete y qué es lo que ocurre por debajo.
Una vez se entiende el proceso, todo cobra más de sentido.
Asegúrate que el intérprete de Python tiene acceso a cada paquete o módulo que estés tratando de importar. Si no es así, modifica el array sys.path para añadir el directorio en cuestión, o hazlo modificando la variable de entorno PYTHONPATH
.