Codetail

Article 14 of 15

Modules & Imports

Code organization, packages, the import system.

14 min read

What is a module?

A module is any .py file. When you write import math, Python finds math.py somewhere on its search path, executes it, and returns the result as a module object. Everything defined at the top level of that file becomes an attribute on the object.

Python
1import math
2
3print(math.pi) # 3.141592653589793
4print(math.sqrt(16)) # 4.0
5print(math.floor(3.7)) # 3
6
7# The module is an object you can inspect
8print(type(math)) # <class 'module'>

Your own modules

Any file you write is importable as a module. Save this as utils.py and import it from any file in the same directory.

Python
1# utils.py
2def add(a, b):
3 return a + b
4
5def clamp(value, lo, hi):
6 return max(lo, min(value, hi))
7
8TIMEOUT = 30 # module-level constant
Python
1# main.py (same directory as utils.py)
2import utils
3
4print(utils.add(2, 3)) # 5
5print(utils.clamp(150, 0, 100)) # 100
6print(utils.TIMEOUT) # 30

The __name__ guard

Every module has a built-in __name__ attribute. When Python runs a file directly, __name__ is set to "__main__". When the same file is imported by another module, __name__ is set to the module's name instead.

Python
1# utils.py
2def add(a, b):
3 return a + b
4
5if __name__ == "__main__":
6 # This block only runs when executing utils.py directly.
7 # It does NOT run when utils is imported.
8 assert add(2, 3) == 5
9 assert add(-1, 1) == 0
10 print("All tests passed.")

This guard is the standard way to make a file work as both an importable library and a runnable script. Without it, any demo or test code at the bottom of your file runs every time someone imports the module.

Import syntax

Python has four import forms. Each serves a different purpose.

import module

Imports the whole module. You access names through the module object using dot notation. This keeps your local namespace clean and makes every name's origin obvious at a glance.

Python
1import os
2import json
3
4home = os.path.expanduser("~")
5data = json.loads('{"key": "value"}')
6
7# The source is unambiguous, you always know where a name came from
8print(os.getcwd()) # /home/user/project

from module import name

Imports specific names directly into your namespace. Use this when you need only one or two things from a large module and the context is clear without the module prefix.

Python
1from math import sqrt, pi
2from pathlib import Path
3from datetime import datetime, timedelta
4
5print(sqrt(25)) # 5.0
6print(pi) # 3.141592653589793
7
8# Multiple imports from the same module can span lines
9from os.path import (
10 join,
11 exists,
12 dirname,
13)

import module as alias

Aliases shorten long module names. The data science ecosystem has settled on a few conventions, use them so your code looks familiar to everyone else.

Python
1import numpy as np # universal convention
2import pandas as pd # universal convention
3import matplotlib.pyplot as plt
4import datetime as dt # useful when the full name is repetitive
5
6arr = np.array([1, 2, 3])
7today = dt.date.today()

Wildcard imports, avoid them

from module import * dumps every public name from the module into your current namespace. This makes it impossible to tell where any name came from, and it silently overwrites names that were already defined.

Python
1# Bad, where does chain come from? Where does sqrt come from?
2from itertools import *
3from math import *
4
5result = chain([1, 2], [3, 4]) # itertools? math? your own code?
6val = sqrt(9) # ambiguous
7
8# Good, every source is explicit
9from itertools import chain
10from math import sqrt

The only accepted use of wildcard imports is inside __init__.py to re-export a curated public API, controlled by __all__.

Packages

A package is a directory that contains an __init__.py file. That file marks the directory as a package and can be empty or expose the package's public API. Packages let you organize related modules into a hierarchy that mirrors your project's structure.

Python
1# A typical project layout
2myapp/
3 __init__.py # makes myapp a package
4 models.py
5 utils.py
6 api/
7 __init__.py # makes api a nested package
8 routes.py
9 auth.py
Python
1# Importing from a package
2import myapp.models # full dotted path
3from myapp import utils # module from a package
4from myapp.api import routes # module from a nested package
5from myapp.api.auth import verify_token # name from a nested module

__init__.py as a public API

When someone imports your package, they see what __init__.py exposes. Use it to pull the most important names up to the top level so callers don't need to know your internal file structure.

Python
1# myapp/__init__.py
2from .models import User, Product # re-export from submodules
3from .utils import format_currency
4
5# Now callers can write:
6from myapp import User
7# instead of navigating the internals:
8from myapp.models import User

__all__ controls what gets exported

__all__ is a list of names that from module import * will include. Even if you never use wildcard imports yourself, it serves as documentation: it tells readers which names are part of the public interface.

Python
1# myapp/utils.py
2__all__ = ["format_currency", "parse_date"] # public API
3
4def format_currency(amount, symbol="$"):
5 return f"{symbol}{amount:.2f}"
6
7def parse_date(s):
8 from datetime import datetime
9 return datetime.strptime(s, "%Y-%m-%d")
10
11def _build_query(params): # underscore = private by convention
12 pass

Namespace packages

Python 3.3 and later support namespace packages: directories without __init__.py. They are useful for splitting one logical package across multiple directories or repositories. For ordinary projects, always include __init__.py. It is explicit and avoids surprising behavior.

Relative imports

Inside a package, you can import from sibling modules using dots. One dot means the current package. Two dots mean the parent package. Relative imports only work inside packages, not in scripts you run directly.

Python
1# Project structure:
2# myapp/
3# __init__.py
4# models.py
5# utils.py
6# api/
7# __init__.py
8# routes.py
9# auth.py
10
11# Inside myapp/api/routes.py:
12from . import auth # sibling module in same package
13from .auth import verify_token # specific name from sibling
14
15from .. import models # module in parent package
16from ..utils import format_date # specific name from parent module

Absolute vs relative, which to prefer

Absolute imports are explicit and work from anywhere. Relative imports are shorter inside deeply nested packages. The Python community generally prefers absolute imports, they survive renaming and restructuring without breaking.

Python
1# myapp/api/routes.py
2
3# Absolute, always clear, always works
4from myapp.models import User
5from myapp.utils import format_date
6
7# Relative, shorter, but breaks if you move or rename the package
8from ..models import User
9from ..utils import format_date

If you run a file directly and it uses relative imports, Python raises ImportError: attempted relative import with no known parent package. Relative imports require the file to be part of an installed package, not a standalone script.

Circular imports

A circular import occurs when module A imports B, and B imports A. Python partially executes A before finishing B's import of A, which gives B an incomplete view of A. The result is usually an ImportError or a missing attribute at runtime.

Python
1# a.py, imports from b
2from b import greet
3
4def name():
5 return "Alice"
6
7# b.py, imports from a (circular!)
8from a import name
9
10def greet():
11 return f"Hello, {name()}"
12# ImportError: cannot import name 'name' from partially initialized module 'a'

Fix circular imports by moving shared code into a third module that both A and B import, or by deferring the import inside the function where it is actually used.

Python
1# shared.py, neither a.py nor b.py imports the other
2def name():
3 return "Alice"
4
5# b.py, imports only from shared
6from shared import name
7
8def greet():
9 return f"Hello, {name()}"
10
11# a.py, imports only from shared and b
12from shared import name
13from b import greet
14
15print(greet()) # Hello, Alice

How Python finds modules

When you write import utils, Python searches a list of directories called sys.path in order. It imports the first match it finds. If nothing matches, you get ModuleNotFoundError.

Python
1import sys
2
3for path in sys.path:
4 print(path)

Python searches in this order:

  1. The directory containing the script being run (or the current directory in the REPL)
  2. Directories listed in the PYTHONPATH environment variable
  3. Standard library directories
  4. Site-packages (where pip installs third-party packages)

Why ModuleNotFoundError happens

The most common causes:

Python
1# 1. Package not installed in this environment
2import requests # fix: pip install requests
3
4# 2. Wrong virtual environment, always check which Python you're using
5import sys
6print(sys.executable) # shows the exact binary
7# /home/user/wrong-project/.venv/bin/python
8
9# 3. Module file is in the wrong location relative to the script
10# script at: project/scripts/run.py
11# module at: project/utils.py
12# Python only looks in project/scripts/, not project/
13# fix: structure as a package and install it properly

Never modify sys.path in production code. Use a proper package structure and install it in development mode with pip install -e . so Python finds it the right way.

Module caching

Python caches every imported module in sys.modules. Importing the same module a second time does not re-execute its code, it returns the cached object immediately. This is why import side effects only run once.

Python
1import sys
2import math
3
4# Already cached from the import above
5print("math" in sys.modules) # True
6print(sys.modules["math"] is math) # True, same object, not a copy