Modules & Imports

6 min read

Reading Progress0%
CS Fundamentals Index
Tier 1 -- Foundations
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
CS Fundamentals Index
Tier 1 -- Foundations
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs

Modules & Imports

1. What Is It?

A is a file (or package) of related code you can pull into another file by name. Imports are the mechanism for doing that pulling. Without modules, every program is one giant source file, nothing is reusable, and naming collisions are inevitable.

The concept solves three problems at once: reuse (one math library, used by everyone), namespacing (your parse and mine don't collide), and (exposing a deliberate API while hiding the rest).

QUICK CHECK

Two backend engineers at the same company each write a utility function called parse() in their respective services. When both functions are eventually needed in a shared codebase, there is no naming conflict. Which property of the module system is responsible for preventing this collision?

Choose one answer

2. How It Works

At its core, importing does two things:

  1. Find and load the — locate the file on disk, compile/parse it, execute any top-level code.
  2. Bind names into the importing so you can refer to the 's contents.
# Python — file math_utils.py
PI = 3.14159
def area(r): return PI * r * r
# Python — in another file
import math_utils                            # load, bind 'math_utils'
print(math_utils.area(2))

from math_utils import area, PI              # bind specific names
from math_utils import area as compute_area  # rename

import math_utils as mu                      # whole-module alias
// Java — file: src/geom/Circle.java
package geom;
public class Circle {
    public static double area(double r) { return Math.PI * r * r; }
}
// Java — elsewhere
import geom.Circle;            // make Circle visible by short name
double a = Circle.area(2);

import static geom.Circle.area; // bring the static method itself into scope
double b = area(2);
// C++ — header file: circle.h
#pragma once
double area(double r);

// circle.cpp
#include "circle.h"
double area(double r) { return 3.14159 * r * r; }

// main.cpp
#include "circle.h"
int main() { return area(2); }

#include is not import

C++'s #include is a preprocessor directive that pastes the header file's text into the including file. The compiler then compiles the combined result. This is why you need include guards (#pragma once or #ifndef ... #define ...) — without them, a header included twice defines everything twice.

Modern C++ (modules introduced in C++20; import std; standardized in C++23) provides real modules that behave more like Python/Java — but #include remains standard in most codebases today.

Python import mechanics

When Python sees import foo:

  1. Check sys.modules — already loaded? Return the cached module.
  2. Search sys.path (a list of directories) for foo.py or foo/ package.
  3. Execute the module's top-level code once; cache in sys.modules.
  4. Bind foo in the current namespace.

Top-level code runs at import time, not just when you call a function inside. Print statements, side effects, network calls — they all fire the first time anyone imports the module.

QUICK CHECK

A backend developer writes a Python module that establishes a database connection at the top level of the file (outside any function). Another module imports it twice in the same process. How many times will the database connection be established?

Choose one answer

3. What You Actually Need to Know

  • Import once, used everywhere. Python caches modules in sys.modules. A second import foo is a dict lookup, not a re-execution. This is also why editing a isn't seen by a running interpreter — you need to restart or use importlib.reload.
  • Avoid from import *. It dumps every public name into your , causing collisions and making it impossible to trace where a name came from. Prefer explicit imports.
  • Circular imports happen when a.py imports b.py and b.py imports a.py. Python partially loads one, hits the second, and you get a half-initialized module with some names missing. The fix is usually restructuring (move shared code into a third module) or deferring the import to inside a function.
  • Package vs. module. A module is a single file. A package is a directory containing an __init__.py (Python) or a namespace (Java package, C++ namespace). Packages let you group related modules and nest them.
  • Java's package = directory. Package com.example.geom must live in com/example/geom/. The compiler enforces this.
  • C++ header/implementation split. Headers declare what exists (signatures, definitions). .cpp files implement them. Headers are for everyone who uses your code; implementations get compiled separately and linked.
  • Debugging clues:
    • ModuleNotFoundError / ImportError (Python) → wrong path, missing __init__.py, or name collision with a local file.
    • ClassNotFoundException (Java runtime) → wasn't on the classpath.
    • C++ linker error undefined reference → you declared something in a header but never compiled/linked its implementation.
    • Surprising side effects at startup → module's top-level code runs on import; move heavy work into functions.
QUICK CHECK

Your Python web application imports a configuration module at startup. A teammate edits that module to update a database URL, but the running server still uses the old URL. What is the most likely reason?

Choose one answer

4. Language Differences

AspectPythonJavaC++
UnitModule (file) or package (directory)Class (usually one per file) in a packageHeader + translation unit; C++20 modules
Import syntaximport, from ... importimport ...; (at file top)#include "..." or import ...; (C++20)
Loaded at runtime or compile time?RuntimeCompile + runtime (class loader)Preprocessing (for #include); compile/link
Can partially import?Yes (from m import foo)Yes (import pkg.Foo)No — #include pastes the whole file
Name pollution riskfrom m import *import pkg.*;Every symbol in the header
Circular import behaviorPartially-loaded moduleCompile error or runtime NoClassDefFoundErrorLinker / duplicate-definition errors; needs forward declarations

Python's "execute at import" model is the most surprising of the three — a isn't just declarations; it's a script that runs the first time it's imported.

QUICK CHECK

A backend developer is building a shared utilities library and notices that after switching from Python to C++, adding a new helper function to a shared header file causes a significant increase in compile times across many unrelated modules. Which fundamental difference in how C++ handles #include versus Python's import best explains this problem?

Choose one answer

5. Tradeoffs & Decisions

  • Explicit imports vs. star imports. Explicit is almost always better — grep-ability, no collisions, IDE autocomplete. Star imports are OK for a small, well-known utility or inside a REPL.
  • Aliasing. import numpy as np is a convention that saves typing and makes code portable across projects. Aliasing is good when the full name is long or clashes.
  • Import location: top of file vs. inside function. Top-of-file is standard — it's clear what the file depends on. Deferred (inside-function) imports are justified only for resolving circular imports or avoiding expensive loads at startup.
  • Package boundaries as API contracts. What's exported vs. internal matters. Python has __all__ and leading underscores as conventions. Java has public/package-private/private. C++ separates .h (public) from .cpp (internal) and uses anonymous namespaces for file-local functions.
  • Dependency depth. A that transitively imports thousands of lines takes longer to load and is a maintenance liability. Keep imports shallow where possible.

If you see a circular import, it usually means two modules should be one, or a third module should hold their shared code. You'd choose deferred imports only when restructuring isn't possible.

QUICK CHECK

A backend service imports a heavy analytics library at the top of its main module, but that library is only used in one rarely-called admin endpoint. Startup time is noticeably slow, and the team can't easily restructure the code. What is the most justified approach?

Choose one answer

6. Interview Cheat Sheet

  • A is a reusable unit of code (file or package); imports bind names from that into your current .
  • Python imports execute the module's top-level code once and cache the result in sys.modules — subsequent imports are cheap.
  • C++ #include pastes text; it's a preprocessor operation, not a true import. Hence header guards and the header/.cpp split.
  • Circular imports happen when two modules need each other at load time; fix by restructuring or deferring.
  • Avoid from m import * — it pollutes the namespace and hides the source of names.
  • Package boundaries are your API contract — be deliberate about what's public vs. internal.

Follow-ups:

  • "What does import actually do in Python?" — Locate the module on sys.path, execute its top-level code once, cache in sys.modules, and bind the module name in the caller's namespace.
  • "Why do C++ headers need include guards?"#include pastes the header text; without guards, including a header transitively twice would redefine everything and cause a compile error.
  • "How do circular imports break?" — The first module is partially loaded when the second tries to use it, so some names it needs don't yet exist.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.