AlgoMaster Logo

Modules Basics

High Priority27 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A module is the unit Python uses to organize code across files. Once a program grows past a few dozen lines, putting everything in one file stops being practical, and splitting work into modules is how Python keeps it tidy. This lesson covers what a module actually is, why you'd split code into modules, the metadata Python attaches to every module, and how to peek at a module's contents.

What a Module Actually Is

A module is any Python file. That's it. The file pricing.py is a module called pricing. The file customer.py is a module called customer. The file you're running right now is also a module. Python doesn't draw a hard line between "the script I'm running" and "a library I imported"; both are modules, and the rules that govern them are the same.

The point of a module is to bundle related code (functions, variables, classes) under one name so the rest of the program can use it without copying anything. When you have a function that calculates a cart total, you don't want to retype it in every file that needs it. You write it once in pricing.py and let any other file pull it in.

Start with a tiny module. Create a file called pricing.py next to your main script:

Then, in another file in the same folder, called shop.py, use it:

The import pricing line tells Python "go find a module named pricing and load it". After that, every name defined inside pricing.py becomes available as an attribute of pricing. The function cart_total becomes pricing.cart_total, and the constant TAX_RATE becomes pricing.TAX_RATE. The dot is the access operator, the same way it works for any other Python object.

This is the smallest useful picture of a module: one file defines names, another file imports those names through the file's name. For now, plain import modulename is enough to make every example below work.

Why Split Code Into Modules

A single-file program works until it doesn't. The first time you need to find a function you wrote three weeks ago and have to scroll through 800 lines to find it, the pain is obvious. Splitting code into modules solves three real problems at once: reuse, organization, and namespacing.

Reuse

If two scripts both need to calculate a cart total, they have two options: each copies the function, or both import it from a shared module. Copying means two places to fix when the tax rules change, and two places to drift out of sync. Importing means one place to fix, one place to update.

Both checkout.py and email_receipts.py lean on the same cart_total. When the tax rate changes, you edit pricing.py and both files immediately see the new behavior. No copy-paste bugs, no forgotten files.

Organization

A 50-line file is easy to scan. A 500-line file is a chore. A 5,000-line file is where bugs live. Splitting by purpose makes each file small enough to read in one sitting:

FileWhat lives there
pricing.pyTax, discounts, totals
customer.pyCustomer records, addresses
cart.pyCart structure, add and remove operations
orders.pyOrder placement, status updates
shop.pyThe entry point that ties it together

Each file is small. Each file has a clear job. When you need to fix a discount bug, you open pricing.py and don't have to wade through 4,000 lines of unrelated order-status code on the way.

Namespacing

Modules give names a place to live. Without modules, every function and variable in your program shares a single global pool, and collisions become inevitable. With modules, two files can both have a function called total without stepping on each other, because one is pricing.total and the other is shipping.total.

Both files define a function named total. They don't clash because each lives behind the module's name. The module acts like a folder for names, and the dot acts like the slash in a file path.

The diagram shows the typical shape: one entry-point file (cyan) leans on several smaller files (orange), each holding one slice of the program. Every arrow is an import. The entry-point file doesn't define much itself; it stitches together work that lives elsewhere.

Importing a Module (The Basics)

To use names from another module, you import it. The most direct form is the bare import statement:

This does three things, in order:

  1. Python finds a file called pricing.py.
  2. Python runs the file from top to bottom, just like running it as a script, except the names defined inside stick around in a module object.
  3. Python binds the name pricing in the current file to that module object.

After the import, you reach names inside the module with the dot:

The dot tells Python "look inside pricing for an attribute called TAX_RATE". A module is just an object, and the names defined inside it are its attributes. That's the same mechanism Python uses for any other dotted name (some_list.append, some_string.upper), so the syntax feels consistent once you've seen it for modules.

There are other shapes of import (from pricing import cart_total, import pricing as p, and so on). They're useful, but they're variations on the same idea and they get their own lesson. For everything in this chapter, plain import modulename does the job.

One quick rule: an import statement at the top of a file makes the module available for the rest of that file. By convention, imports go at the very top, above any other code. Putting them anywhere else is legal Python, but it makes the file harder to read because someone scanning the top can't see what the file depends on.

Module vs Script: The Same File, Two Roles

A .py file can do double duty. The same file can be run directly as a script (python pricing.py) or imported by another file (import pricing). Python doesn't force you to pick one or the other; the file is just a module, and you can use it either way.

When you run a file directly, Python loads it and executes every top-level statement. When you import the same file, Python also loads it and executes every top-level statement, the difference is what happens afterward. A script runs and exits. An imported module sticks around as an object that the importing file can poke at.

If you run python pricing.py directly:

If you run python shop.py where shop.py contains import pricing:

The print runs in both cases, because both cases execute the top-level code. The difference is harder to see: in the first case, the module ends and Python exits. In the second case, the module ends and Python keeps going, with pricing now sitting in memory ready for shop.py to use.

This dual nature is why most non-trivial modules guard their "if I'm being run as a script" code with the line if __name__ == "__main__":. That idiom lets a file both define reusable helpers and offer a runnable entry point, without one stepping on the other. For this lesson, the takeaway is just that a .py file is a module either way, and merely importing it runs its top-level code.

The diagram shows the two paths the same file can take. Both paths run the top-level code (teal). The script path (orange) ends after that. The import path (green) keeps the module object around so the importer can use its names.

Module Attributes: The Metadata Python Attaches

Every module Python loads gets a small bundle of metadata attached to it. These attributes are named with double underscores on each side, which Python calls dunder names (short for "double underscore"). They describe the module to anyone who asks: what it's called, where it lives, what it's about.

The four worth knowing on day one are __name__, __doc__, __file__, and __loader__.

__name__

__name__ is the module's name as Python sees it. For an imported module, it's the import name. For the file you ran directly with python somefile.py, it's the special string "__main__".

If you run python pricing.py directly:

If you import it from another file (import pricing):

The same line prints two different values depending on how the file got loaded. That's the hook the if __name__ == "__main__": idiom hangs on. For now, just know that __name__ is the attribute that lets a file tell whether it's the script being run or a module being imported.

You can also read __name__ from an already-imported module by going through the module object:

(The first line comes from the print at the top of pricing.py, which runs when the import loads the file. The second line is shop.py asking for pricing.__name__ directly.)

__doc__

__doc__ holds the module's docstring: a string literal placed at the very top of the file. If the file doesn't have one, __doc__ is None.

The docstring is just a regular triple-quoted string at the top of the file, but Python treats it specially and binds it to __doc__. That's also what the help() builtin reads when you ask it for help on the module. Documenting your own modules with a one-line summary at the top costs almost nothing and pays off the next time someone (including future you) runs help(pricing).

__file__

__file__ is the absolute path to the source file that backed the module. It's a string and it usually ends in .py.

__file__ is useful for modules that need to find files next to themselves: a config file in the same directory, a data file shipped alongside the code. You can extract the folder with the pathlib or os.path modules and build paths relative to it. The point is that __file__ exists and tells you where the module came from.

A handful of special modules (mostly built-in ones written in C, like sys) don't have a __file__ attribute because they're not loaded from a .py file at all. Reaching for __file__ on those raises AttributeError. For ordinary Python modules you write, it's always there.

__loader__

__loader__ is the object Python used to load the module. Most of the time it's an internal Python detail you don't touch, but it's worth a glance so the name isn't a mystery when you see it.

For a regular .py file, the loader is a SourceFileLoader. For built-in modules, it's a different class. For modules inside a .zip file or a custom import location, it's something else again. The importlib machinery lives behind this attribute. You rarely interact with it directly, but library authors who need to load modules from unusual places do.

All Together

Here's a small inspection script that prints all four for a module of your own:

The !r in the f-string shows __doc__ with its quotes and newlines intact, which makes the structure of the string easier to see.

Modules carry other dunder attributes too (__package__, __spec__, __builtins__, __cached__), but those four are the ones to know first. The rest become relevant when you start building packages or doing import-system tricks, and we'll meet them then.

Inspecting a Module With dir()

The builtin dir() returns a sorted list of the names defined on an object. When you point it at a module, you get a list of every name the module exposes: the functions, classes, variables, and the dunder attributes.

The dunder names at the start are the metadata Python attaches to every module. The names without underscores (TAX_RATE, apply_discount, cart_total) are the ones the module's author defined. Those are the names the rest of your program would use.

dir() is the fastest way to answer "what's in this module?" without opening the file. It's especially handy in the REPL when you've imported something new and want to see what's available before reading the docs:

The REPL flow is: import the module, run dir() on it to see the names, pick a name to investigate, and either inspect it or call it. This loop is one of the small everyday joys of Python; you can poke at a new library without reading a single line of its docs and get a decent feel for what's there.

To strip the dunder noise, filter the list with a comprehension:

That's a cleaner view of what the module actually offers, minus Python's bookkeeping. Names that start with an underscore are conventionally "internal" anyway, so dropping them gives you the public surface of the module.

dir() works on more than modules. Pass it any object and it returns the names defined on that object: a list's methods, a string's methods, a class's attributes. The same one builtin answers "what can I do with this thing?" for almost anything.

Modules Are Cached: Imported Once, Reused Forever

Python imports a module exactly once per process, no matter how many times you write import for it. The first import runs the file from top to bottom and stores the resulting module object. Every later import of the same name just hands back the same object.

That behavior matters because it means a module's top-level code only runs once. Heavy setup, file reads, or configuration loading at the top of a module pays its cost on the first import and is free afterwards.

Three import pricing lines, one print. The first import loaded the file and ran the top-level code. The second and third imports saw that pricing was already loaded and reused the cached object. You can prove they all point to the same thing:

The is operator checks object identity. Both names refer to the same module object in memory. There's no second copy.

This caching has a real consequence for how modules are designed. A module's top-level code is effectively a one-time setup block: register configuration, build lookup tables, create constants. Anything you put there happens once for the life of the program. If a module's top-level code accidentally has a side effect (writing a file, sending a request, mutating shared state), that side effect happens once on first import and never again, which is usually a surprise.

The diagram shows the two paths: the first import does real work (orange and green), saves the result in the cache (teal), and returns it. Every later import skips straight to the cache. The cache itself lives in sys.modules, which is a dictionary that maps module names to module objects.

For now, three things to remember about the cache:

  1. The first import of a module runs its top-level code; later imports do not.
  2. All imports of the same module name in the same process get the same module object.
  3. Mutating a module's attributes through one importer is visible to every other importer, because they're all pointing at the same object.

The third point is subtle and powerful. If pricing.TAX_RATE is 0.08 and one part of your program changes it to 0.10, every other part that uses pricing.TAX_RATE sees 0.10 from then on. This is how Python's "modules as singletons" model lets configuration spread across files without any special framework. It's also how it sometimes bites you, because surprise shared state is still surprise shared state. Use it deliberately, not by accident.

A Slightly Bigger Example: A Two-Module Store

Putting the ideas together, here's a small two-module e-commerce setup. One module owns pricing rules; another module owns customer lookups; a third file is the entry point that uses both.

Each module has one job. pricing.py knows about money. customer.py knows about customers. shop.py doesn't define any business logic; it just stitches the pieces together. If the tax rate changes, only pricing.py changes. If customer storage moves from a hard-coded dictionary to a database lookup, only customer.py changes. The entry point stays put.

If you want a quick look at what each module exposes, you don't need to open the files:

That's the loop again: import, list, look. Three lines of code give you a map of what each module is for. With a docstring at the top of each file, even the why is there.

Quiz

Modules Basics Quiz

10 quizzes