Every non-trivial Python program splits its code across multiple files and pulls those files back together with import. The statement looks small, but it carries a lot of behavior: locating the file, executing it once, caching the result, and binding names into the current scope. This lesson walks through every form of import you'll see in real code, where Python looks for the file, what happens when the import succeeds (or fails), and the conventions PEP 8 expects you to follow.
import module and Qualified AccessThe simplest form of import is import followed by a module name. The module gets loaded, and a name pointing to the module object is bound in the current scope. You then reach into the module using dot notation.
import math doesn't pull sqrt or pi into your file's namespace. It pulls the math module itself in, under the name math, and you reach the contents through math.something. That qualified access is the point: when you read math.sqrt(16), you know exactly where sqrt came from. No guessing.
The same pattern works for modules you write yourself. Suppose you have an e-commerce app with a file named pricing.py sitting next to your main script.
import pricing runs pricing.py once and binds the resulting module to the name pricing in main.py. Functions, classes, and module-level variables are all reached the same way: pricing.apply_discount, pricing.TAX_RATE, and so on.
There's a small detail worth being explicit about: the name you bind is the module's name, not the file path. import pricing works because pricing.py is on Python's search path. You never write import pricing.py and you never write import "./pricing.py". Python takes a bare identifier and figures out which file (or package) that maps to.
from module import nameThe second form pulls specific names out of a module and binds them directly into your current scope. You don't need the qualifier anymore.
from math import sqrt, pi runs math (just like import math would), then takes only sqrt and pi out of it and binds those two names directly. The module itself is not bound, so math.sqrt would now fail with NameError.
This form is convenient when you use a few names from a module repeatedly. Calling apply_discount ten times reads better than pricing.apply_discount ten times.
The trade-off is information loss. When you read apply_discount(99.99, 10) halfway through a file, you can't tell at a glance whether that function came from pricing, from some other local module, or from a third-party library. With import pricing and pricing.apply_discount(...), the source is right there in the call. The right choice depends on how often you use the names and how readable the qualified form is in context.
You can import any number of names in one from statement, separated by commas. For long lists, wrap them in parentheses across multiple lines.
This isn't a tuple. The parentheses are purely syntactic; they let the statement span multiple lines without backslash continuation. Most linters and formatters prefer this layout once a single line gets long.
Cost: Both import math and from math import sqrt execute the module body once. Picking one form over the other doesn't change loading cost. The difference is only in what names get bound in your scope.
asSometimes the name you'd be bound to isn't what you want. Maybe it's too long. Maybe it conflicts with another name you're already using. The as keyword renames the binding at import time.
import numpy as np loads the numpy module and binds it under the name np in the current scope. The name numpy is not bound. This pattern is so ingrained in the data ecosystem that you'll see import pandas as pd and import matplotlib.pyplot as plt in nearly every example online. The aliases are conventions, not rules, but following them makes your code instantly familiar to other readers.
You can also alias individual imports inside a from statement.
apply_discount is now reachable as discount, and TAX_RATE as tax. The original names aren't bound. The most common practical use is conflict resolution: if your file already has a function called apply_discount and you also want one from pricing, you alias the imported one to avoid shadowing your local definition.
Without the alias, the from pricing import apply_discount would clobber the local apply_discount (or vice versa, depending on order), and the result would be a silent bug. The alias makes the two functions distinct names.
from module import * and __all__There's a fourth form: from module import *. It pulls every public name out of the module and binds each of them directly into your scope.
This looks convenient at first glance. You skip writing out names; everything is just there. But the cost is real. You don't know what names got bound, you don't know whether any of them conflict with names you already had, and a reader of your code has no way to tell where sqrt or cos came from.
Here's the classic problem.
Both math and cmath define sqrt. The second import silently shadowed the first. If you'd expected math.sqrt(-1) to raise (which it does for negative input), you wouldn't get that behavior, because sqrt now refers to the complex-number version. The bug is invisible at the call site.
For these reasons, **PEP 8 discourages from module import * in production code**. It's tolerable in interactive sessions or quick scripts where you're exploring, but in any file you'll read again, name your imports.
The __all__ attribute controls what import * exports. If a module defines a list named __all__, only those names are pulled in by import *. Without __all__, every name not starting with an underscore is exported.
Even though INTERNAL_CONSTANT doesn't start with an underscore, it isn't in __all__, so import * doesn't bring it in. __all__ is also the closest thing Python has to a "public API" declaration. Tools, IDEs, and documentation generators read it to know which names a module officially exposes.
Cost: from module import * doesn't just pollute the namespace, it makes static analysis harder. Linters and type checkers can't always tell which names are defined, so warnings about "undefined name" become unreliable.
sys.pathWhen you write import pricing, Python doesn't search your whole disk. It walks through a specific, ordered list of directories called sys.path. The first directory containing a pricing.py (or a pricing/ package) wins.
The exact contents vary by system and Python install. What matters is the order. sys.path is built from three sources, roughly in this order:
"" meaning current working directory, when you're in the REPL).PYTHONPATH environment variable, if set.site-packages directory where pip installs third-party packages.Python takes the first match it finds. If pricing.py exists in both your project directory and somewhere on PYTHONPATH, the project directory wins because it comes first. This is mostly invisible, but it occasionally bites: if you name a local file math.py, you've shadowed the standard library math, and import math will load your file instead.
The standard library lives in directories that came with Python. site-packages is where pip install requests puts the requests package. You don't normally interact with these paths directly; the import system does it for you. But knowing the order helps when an import behaves unexpectedly: print sys.path and trace where Python would have looked.
You can modify sys.path at runtime, though it's a code smell in most situations. The cleaner ways to make your code importable are: organize files into packages, install with pip, or set PYTHONPATH outside Python. sys.path modification is mentioned here mainly so the mechanism isn't a mystery.
This works, but if you find yourself reaching for it often, the project layout is probably the real problem.
sys.modulesPython only runs a module's body once per process, no matter how many times it's imported. The first import does the work; every later import just returns the already-loaded module.
The cache lives in sys.modules, a dictionary mapping module names to module objects.
The print inside pricing.py runs exactly once. The second and third import pricing statements find pricing already in sys.modules and bind the cached module object without re-executing the file.
This caching is what makes import statements cheap. Once a module is loaded, importing it again costs about as much as a dictionary lookup. It also means top-level code in a module behaves like initialization: it runs once and sets up state, and every other file in the program sees the same state.
You can inspect the cache directly.
The module object stored in sys.modules["pricing"] is the exact same object bound to the name pricing in your scope. Every other file that imports pricing gets that same object too. There's only one pricing module per process.
This has a practical consequence. If you edit pricing.py while a Python session is running, just re-running import pricing won't pick up your changes. The cache says "already loaded", so the new file content is ignored. For long-running sessions (notebooks, REPLs), use importlib.reload.
importlib.reload re-executes the module body and updates the cached object in place. Other modules that already imported names from pricing may still hold references to the old definitions, though, which makes reload unreliable for anything beyond simple cases. In production code you usually restart the process instead.
So far every import has used an absolute name: the full path to the module from the top of the package structure. Absolute imports are the default and the recommended style for almost all cases.
These work no matter where the importing file lives, because the path is fully qualified. shop.pricing always means the pricing module inside the top-level shop package.
Inside a package, you can also use relative imports, which reference modules by their position relative to the current file. A relative import starts with one or more dots: . means "the current package", .. means "the parent package", and so on.
The single dot in from .items import CartItem says "look in the same package as this file". Since checkout.py lives in shop/cart/, the import resolves to shop.cart.items.
The double dots in from ..pricing import apply_discount say "look one package up". That resolves to shop.pricing.
The dashed arrows show what each relative import resolves to. .items stays in cart/. ..pricing walks up to shop/ and finds pricing.py there.
Relative imports only work inside packages. If you try a relative import in a top-level script (one you run directly as python checkout.py), you'll see this:
The error means Python can't figure out what "the current package" is, because the file is being run as the main script, not imported as part of a package. The fix is either to switch to absolute imports or to run the file as a module: python -m shop.cart.checkout.
When to use which:
| Style | Example | Best for |
|---|---|---|
| Absolute | from shop.pricing import apply_discount | Almost everything. Clear, unambiguous, works from anywhere. |
| Relative | from .items import CartItem | Sibling imports inside a package, especially when the package might be renamed. |
PEP 8 recommends absolute imports as the default. Relative imports are acceptable inside a package when they keep things short and the file is firmly part of a package structure. Avoid going more than one level up (... or ....) because it gets hard to follow and often signals a layout problem.
Here we're only looking at the import syntax.
Imports fail in a handful of recognizable ways. Recognizing the error type is half the fix.
ModuleNotFoundErrorThe module you asked for doesn't exist anywhere on sys.path.
Common causes:
shiping_calculator vs shipping_calculator).sys.path (run import sys; print(sys.path) to see).pip install package-name).sys.path.ModuleNotFoundError is a subclass of ImportError, so catching ImportError catches both.
ImportErrorThe module exists, but the specific name you tried to import from it doesn't.
math loaded fine, but it has no name called square_root. The fix is either to use the correct name (sqrt) or to check what the module actually exports.
A circular import happens when two modules import from each other, directly or through a chain. Module A imports B, and B (during its own loading) tries to import A. At that moment, A is only partially loaded, so some of its names don't exist yet.
Running python cart.py produces:
The trace shows the problem clearly: cart started loading, hit from pricing import apply_discount, which started loading pricing, which then tried to import calculate_total from cart, which hadn't finished defining calculate_total yet.
Three common fixes, in order of preference:
cart needs pricing and pricing needs cart, one of them probably belongs in a third module both can import. Pull the shared logic out.import pricing inside cart.py (and use pricing.apply_discount) often works where from pricing import apply_discount fails, because the module object exists in sys.modules even when it's partially loaded.The lazy import works because the from pricing import apply_discount line doesn't run at module load time. It runs the first time calculate_total is called, by which point both modules have finished initializing.
Cost: Circular imports usually point to a design issue. Lazy imports inside functions hide the problem but add a tiny per-call cost (a dict lookup in sys.modules). For hot code paths, prefer fixing the structure.
Two style rules show up in nearly every Python codebase, and PEP 8 codifies both.
Rule 1: Put imports at the top of the file. After the module docstring and any future imports, before any module-level code. Imports are statements, and you can technically write them anywhere, but putting them all at the top means a reader can see every external dependency in one glance.
Rule 2: Group imports into three sections, in this order, separated by blank lines.
os, sys, math, ...)requests, numpy, pandas, ...)Within each group, sort the imports alphabetically. This isn't strictly required by Python; it just makes diffs cleaner and conflicts rarer. Tools like isort and ruff will sort imports for you automatically.
The three groups make the import dependency graph readable at a glance: what does this file need from Python itself, what does it need from packages we installed, and what does it need from inside our own project.
There are two situations where imports legitimately appear lower in the file:
For everything else, keep imports at the top.
Before the exercises, here's a compact table of every import form covered in this lesson and what each one binds in your scope.
| Statement | Names bound in your scope | Typical use |
|---|---|---|
import math | math | Default form; clearest source attribution. |
import math as m | m | Aliasing a module (e.g., np, pd). |
from math import sqrt | sqrt | Pulling a few specific names you'll use a lot. |
from math import sqrt, pi | sqrt, pi | Same, multiple names at once. |
from math import sqrt as s | s | Renaming an imported name to avoid conflict. |
from math import * | Everything in math.__all__ or all public names | Avoid in production code. |
from .sibling import x | x | Relative import: same package as current file. |
from ..parent import x | x | Relative import: parent package of current file. |
Every form runs the target module's body once (the first time), caches it in sys.modules, and then binds whichever names the syntax says to bind. The differences are entirely about what ends up named in your scope and how readable the result is.
10 quizzes