Polyglot
Languages Python scope
Python § scope

Modules

Python’s organisation has two principal levels: modules (single source files) and packages (directories containing multiple modules, with an __init__.py or as a namespace package). Within a module, variables have function scope, module scope, or class scope — Python does not have block scope as in C-family languages. The import statement, the conventional namespacing through dotted names, and the package-discovery mechanism together form the import system. Beyond modules at the language level, packaging — distribution, dependency management, virtual environments — is treated separately in Packaging and the build.

This page covers the module mechanism, the import statement, the four scopes (LEGB), and the conventions for naming and organising Python code.

Modules

A module is a single Python source file. The file mymod.py defines the module mymod:

# File: mymod.py

GREETING = "hello"

def greet(name: str) -> str:
    return f"{GREETING}, {name}"

class Counter:
    def __init__(self):
        self.value = 0

    def increment(self):
        self.value += 1

Another file imports it:

# File: main.py

import mymod

print(mymod.greet("alice"))
c = mymod.Counter()
c.increment()

The module’s namespace contains every top-level name defined in the file. Importing the module binds the module object to the name mymod in the importer’s namespace.

The import statement

Several forms:

import math                        # bind 'math' to the module
import math as m                   # alias

from math import pi                # bring pi into scope
from math import pi, e             # multiple
from math import *                 # everything (rare; usually bad)
from math import sin as sine       # rename

# Submodule imports:
from os.path import join, exists
import xml.etree.ElementTree as ET

# Relative imports (within a package):
from . import sibling
from .submodule import thing
from ..parent_package import other

The conventional discipline:

  • Use import module (or import module as m) for libraries you call repeatedly.
  • Use from module import name for specific names used heavily.
  • Avoid from module import * outside the REPL.
  • Use absolute imports by default; reserve relative imports for tight coupling within a package.

When the import happens

Imports are executed at runtime — the imported module’s top-level code runs the first time it is imported:

# in mymod.py:
print("mymod is being imported")    # prints once, on first import

GREETING = "hello"

Subsequent imports of the same module return the cached module object (in sys.modules); the top-level code does not re-run. The mechanism admits initialisation code (registering plugins, computing constants) in the module body.

The implication: import-time side effects are visible. The conventional discipline is to keep top-level module code declarative — function and class definitions, constants — and put substantial logic into functions called explicitly.

if __name__ == "__main__":

A module that may also be run as a script:

def main():
    # ... script logic ...
    pass

if __name__ == "__main__":
    main()

When the file is run as a script (python mymod.py), __name__ is "__main__" and main() runs. When the file is imported as a module, __name__ is "mymod" and main() does not run automatically.

The pattern admits files that work both as importable libraries and as command-line scripts.

Packages

A package is a directory containing modules. Two forms:

Regular packages

A directory with __init__.py:

mypkg/
├── __init__.py        # makes mypkg a regular package
├── core.py            # module mypkg.core
├── util.py            # module mypkg.util
└── sub/
    ├── __init__.py
    └── deeper.py      # module mypkg.sub.deeper

The __init__.py may be empty or may contain initialisation code:

# mypkg/__init__.py
from .core import main_function
from .util import helper

__version__ = "1.0.0"
__all__ = ["main_function", "helper"]

The __init__.py runs when the package is first imported. Re-exporting selected names from submodules admits a clean public API:

import mypkg
mypkg.main_function()       # equivalent to mypkg.core.main_function()

The __all__ controls what from mypkg import * exposes; it does not affect import mypkg.x directly.

Namespace packages (PEP 420)

A directory without __init__.py is a namespace package; the contents are importable but the package is not a regular module:

mypkg/
├── core.py
└── util.py

mypkg.core and mypkg.util are importable. Namespace packages admit splitting a logical package across multiple physical locations on the search path; the conventional contemporary use is for plugin systems where multiple installations contribute to a single namespace.

For most projects, a regular package (with __init__.py) is preferable.

The four scopes (LEGB)

Python’s name resolution follows the LEGB rule:

  • L — Local: defined within the current function.
  • E — Enclosing: defined in any enclosing function (for nested functions).
  • G — Global: defined at module scope.
  • B — Built-in: defined in the builtins module (print, len, int, etc.).
counter = 0           # global

def increment():
    counter = 1       # local; shadows the global

increment()
print(counter)        # 0; the global was unchanged

To modify a global from within a function:

counter = 0

def increment():
    global counter    # declare we modify the global
    counter += 1

increment()
print(counter)        # 1

The global declaration tells Python “treat counter as the module-level variable, not as a new local”.

For nested functions, nonlocal admits modifying an enclosing function’s local:

def make_counter():
    n = 0
    def increment():
        nonlocal n     # not local, not global; the enclosing scope's n
        n += 1
        return n
    return increment

c = make_counter()
c()                    # 1
c()                    # 2

The mechanism admits closures that update their captured state. Without nonlocal, the assignment would create a new local n inside increment, and the closure would not work.

Class scope

Class definitions introduce a class scope during the class body’s execution:

class Widget:
    count = 0          # class attribute

    def __init__(self):
        self.id = Widget.count    # access via class name
        Widget.count += 1

Inside the class body, names refer to the class scope; inside methods, the class is accessed via self.attr (instance attribute) or ClassName.attr (class attribute).

Class scope is not an enclosing scope for methods — a method does not see the class body’s locals directly:

class Widget:
    multiplier = 10

    def compute(self, x):
        return x * multiplier    # NameError: 'multiplier' is not defined
        return x * self.multiplier   # OK
        return x * Widget.multiplier  # OK

The peculiarity is occasionally surprising; the conventional access is through self or the class name.

Imports and namespacing

The principal imports work as follows:

import math
math.sqrt(16)             # 4.0; math is the module, sqrt is its attribute

from math import sqrt
sqrt(16)                   # 4.0; sqrt is now in the local namespace

from math import *         # everything (in math's __all__ if defined)
sqrt(16)

The import form keeps names qualified (math.sqrt); the from … import form brings names directly into scope. The conventional discipline:

  • Prefer import for library-level access (math.sqrt, os.path.join).
  • Use from for heavily-used names within a single function.
  • Avoid from … import * (it pollutes the namespace and obscures the source of names).

Naming conventions

PEP 8 codifies naming:

ConventionUse
snake_caseFunctions, variables, module names
CamelCase (also PascalCase)Classes
SCREAMING_SNAKE_CASEConstants
_leading_underscore”Private” — convention only
__double_leadingName mangling in classes (__x becomes _ClassName__x)
__dunder__Reserved for Python (do not invent new ones)
_single_trailing_Used to avoid collision with reserved words (class_, id_)

The single leading underscore is the conventional Python “private” marker. There is no language-enforced privacy; the convention is honoured by linters and type checkers, and the from module import * form skips names starting with underscore (unless they appear in __all__).

__all__

A module may declare its public API explicitly:

# In mymodule.py:

__all__ = ["main_function", "helper", "Constants"]

def main_function():
    pass

def helper():
    pass

def _internal():
    pass

class Constants:
    pass

The __all__ list controls from mymodule import *: only names in __all__ are exported. The _internal function is not exported (it would be skipped by default anyway because of the leading underscore, but __all__ makes the intent explicit).

The __all__ is documentation; tools (linters, IDEs) use it to determine the module’s public surface.

Module-level code

A module’s top-level code runs once, the first time the module is imported:

# In config.py:

import json

CONFIG_PATH = "config.json"

with open(CONFIG_PATH) as f:
    SETTINGS = json.load(f)

The SETTINGS is computed at import time. The conventional discipline is to keep top-level code declarative — definitions and one-time setup — and avoid side effects that may surprise the importer.

Circular imports

Two modules importing each other produces the circular import problem:

# a.py
import b
def foo():
    return b.bar()

# b.py
import a
def bar():
    return a.foo()

Python imports modules incrementally; if a imports b while a is still being imported, b’s import a returns the partially-initialised a. Names not yet defined produce AttributeError or ImportError.

The conventional defences:

  • Refactor to remove the cycle (extract shared code to a third module).
  • Use late imports (import inside a function rather than at the top of the file).
  • Use type-only imports (with if TYPE_CHECKING: from typing).
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from b import B    # only seen by type checkers; runtime skips

def foo(b: "B"):       # forward reference as string
    pass

The mechanism admits cyclic dependencies in type annotations without runtime cycles.

Common patterns

Re-exports through __init__.py

# mypkg/__init__.py
from .core import API, run
from .config import Config

__all__ = ["API", "run", "Config"]

Users import mypkg and access mypkg.API, mypkg.run, mypkg.Config without knowing the internal organisation.

Selective imports

from os.path import join, exists, dirname
from typing import Optional, Iterator
from collections import defaultdict, Counter

The conventional contemporary form for heavily-used names; selective imports keep the call sites concise.

Conditional imports

try:
    import ujson as json   # faster JSON; not always available
except ImportError:
    import json

The pattern admits using a faster alternative when available and falling back to a standard one.

Lazy imports

def expensive_function():
    import heavy_module    # imported only when this function is called
    return heavy_module.compute()

Lazy imports defer the import until the function is called. The conventional uses are very-large modules (scipy, tensorflow) that should not pay their cost at startup.

Path-based discovery

import os
import sys

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJECT_ROOT)

The pattern admits adding directories to the import path. Modern projects use pyproject.toml and the package model instead; manual sys.path manipulation is conventionally avoided.

A note on the import system

Python’s import system is more elaborate than the surface suggests. Behind import x:

  1. Check sys.modules for x; if present, use the cached module.
  2. Search sys.path for x.py or x/__init__.py.
  3. Execute the module’s body, populating its namespace.
  4. Cache the module in sys.modules.

The mechanism admits finders, loaders, and import hooks (PEP 302). For most use cases, the default behaviour is sufficient; the advanced mechanisms are useful for plugin systems, namespace packages, and frameworks (Django, Flask, pytest) that load modules dynamically.

The full treatment of distribution, dependencies, and virtual environments is in Packaging and the build.