Python & AI Tutorials Logo
Python Programming

22. Organizing Code with Modules and Packages

As your Python programs grow larger, keeping all your code in a single file becomes impractical. You'll want to organize related functions, classes, and variables into separate files that can be reused across different programs. Python's module and package system provides exactly this capability—a way to organize, share, and reuse code effectively.

In this chapter, we'll explore how Python's import system works, how to create and use your own modules, and how to organize multiple modules into packages. We'll also examine the special __name__ variable that lets you write files that work both as importable modules and as standalone scripts.

22.1) What Modules Are and How import Works

Understanding Modules

A module is simply a Python file containing definitions and statements. Any .py file you create is a module. When you write a function in a file called calculator.py, that file becomes a module named calculator that other Python files can use.

Modules serve several important purposes:

  • Code Reusability: Write a function once and use it in multiple programs
  • Organization: Group related functionality together
  • Namespace Management: Keep names separate to avoid conflicts
  • Maintainability: Smaller, focused files are easier to understand and modify

Let's create a simple module to see how this works. Create a file named greetings.py:

python
# greetings.py
def say_hello(name):
    """Return a friendly greeting."""
    return f"Hello, {name}!"
 
def say_goodbye(name):
    """Return a farewell message."""
    return f"Goodbye, {name}. See you soon!"
 
# A module-level variable
default_greeting = "Welcome"

This file is now a module. It contains two functions and one variable that other Python files can use.

The import Statement

To use code from a module, you import it. The import statement tells Python to load a module and make its contents available. Create another file in the same directory called main.py:

python
# main.py
import greetings
 
message = greetings.say_hello("Alice")
print(message)  # Output: Hello, Alice!
 
farewell = greetings.say_goodbye("Bob")
print(farewell)  # Output: Goodbye, Bob. See you soon!
 
print(greetings.default_greeting)  # Output: Welcome

When you run main.py, Python executes the import greetings statement. Here's what happens behind the scenes:

No

Yes

import greetings

Is greetings
already imported?

Search for greetings.py

Execute greetings.py
top to bottom

Create module object
named 'greetings'

Store functions and variables
as attributes

Make 'greetings' available
in current namespace

Use existing
module object

Important: Python executes a module's code only the first time it's imported in a program. Subsequent imports in the same program reuse the already-loaded module. This prevents duplicate execution and saves time.

Accessing Module Contents

After importing a module, you access its contents using dot notation: module_name.item_name. This is similar to how we access string methods like text.upper() or list methods like numbers.append(), as we learned in Chapters 5 and 14.

python
import greetings
 
# Access functions
result = greetings.say_hello("Charlie")
 
# Access variables
greeting = greetings.default_greeting
 
# You can even check what's in a module
print(dir(greetings))  # Lists all names defined in the module

The dot notation makes it clear where each name comes from. When you see greetings.say_hello(), you immediately know this function comes from the greetings module.

Module Search Path

When you write import greetings, how does Python find greetings.py? Python searches for modules in a specific order:

  1. Current Directory: The directory containing the script you're running
  2. PYTHONPATH: Directories listed in the PYTHONPATH environment variable (if set)
  3. Standard Library: Python's built-in module directories
  4. Site-Packages: Third-party packages installed with pip

You can see Python's search path by examining sys.path:

python
import sys
 
for path in sys.path:
    print(path)

Output (example - your actual paths will differ based on your system and Python installation):

/home/user/projects/myproject
/usr/lib/python3.11
/usr/lib/python3.11/lib-dynload
/usr/local/lib/python3.11/site-packages

The first path in the output is the current working directory. Python searches this directory first, so it can find modules in the same directory.

Module Names and File Names

The module name is the filename without the .py extension. If your file is string_utils.py, the module name is string_utils. Module names must follow Python's identifier rules (as we learned in Chapter 3):

  • Start with a letter or underscore
  • Contain only letters, digits, and underscores
  • Cannot be Python keywords
python
# Valid module names (and filenames)
import data_processor      # data_processor.py
import user_auth          # user_auth.py
import _internal_helpers  # _internal_helpers.py
 
# Invalid - would cause errors
# import 2d_graphics       # Can't start with digit
# import my-module         # Hyphens not allowed
# import class             # 'class' is a keyword

Common Pitfall: Shadowing Standard Library Modules

Be careful not to name your modules the same as standard library modules. If you create a file called random.py in your project directory, Python will import your file instead of the standard library's random module, causing confusing errors:

python
# Your file: random.py
def my_function():
    return 42
 
# Another file in your project
import random
print(random.randint(1, 6))  # ERROR! Your random.py doesn't have randint()

To avoid this, check if a name is already used by the standard library before creating a module with that name. You can check by trying to import it in the Python interactive shell. If it imports without error, that name is already taken.

What Happens During Import

Let's examine what actually happens when you import a module. Create a file called demo_module.py:

python
# demo_module.py
print("Module is being loaded!")
 
def greet():
    print("Hello from demo_module")
 
print("Module loading complete!")

Now import it:

python
# test_import.py
print("Before import")
import demo_module
print("After import")
 
demo_module.greet()

Output:

Before import
Module is being loaded!
Module loading complete!
After import
Hello from demo_module

Notice that the print() statements in demo_module.py execute during import. This demonstrates that importing a module runs all its top-level code. Function definitions are stored for later use, but any code outside functions executes immediately.

If you import the same module again in the same program, the loading messages won't appear again:

python
import demo_module  # First import - executes module code
import demo_module  # Second import - uses cached module
import demo_module  # Third import - still uses cached module

Output:

Module is being loaded!
Module loading complete!

The module code runs only once, no matter how many times you import it.

22.2) Different Ways to Import: import, from, and as

Python provides several ways to import modules and their contents. Each approach has different implications for how you access imported names and how they affect your namespace.

Basic import Statement

The basic import statement we've already seen loads the entire module:

python
import math
 
result = math.sqrt(16)
print(result)  # Output: 4.0
 
pi_value = math.pi
print(pi_value)  # Output: 3.141592653589793

With this approach, you always use the module name as a prefix. This makes code very clear—you can always tell where a name comes from.

Importing Specific Names with from

Sometimes you only need one or two items from a module. The from statement lets you import specific names directly into your namespace:

python
from math import sqrt, pi
 
result = sqrt(25)  # No 'math.' prefix needed
print(result)  # Output: 5.0
 
print(pi)  # Output: 3.141592653589793

Now you can use sqrt and pi directly without the math. prefix. This is convenient when you use these names frequently.

Let's see another example with our greetings module:

python
# Using from import
from greetings import say_hello
 
message = say_hello("Diana")  # Direct access
print(message)  # Output: Hello, Diana!
 
# However, say_goodbye is not available since we didn't import it
# say_goodbye("Diana")  # NameError: name 'say_goodbye' is not defined

You can import multiple names in one statement:

python
from greetings import say_hello, say_goodbye, default_greeting
 
print(say_hello("Eve"))      # Output: Hello, Eve!
print(say_goodbye("Frank"))  # Output: Goodbye, Frank!
print(default_greeting)      # Output: Welcome

The Wildcard Import (and Why to Avoid It)

Python allows importing everything from a module using *:

python
from math import *
 
print(sqrt(9))   # Output: 3.0
print(cos(0))    # Output: 1.0
print(pi)        # Output: 3.141592653589793

This imports all public names from the module (names not starting with underscore). While this seems convenient, it's generally considered bad practice because:

  1. Namespace Pollution: You don't know exactly what names you're importing
  2. Name Conflicts: Imported names might overwrite your own variables
  3. Readability: Code readers can't tell where names come from
python
# Problematic example
from math import *
 
# Later in your code...
def sqrt(x):
    """Your own square root function."""
    return x ** 0.5
 
# Which sqrt are you using? Yours or math's?
result = sqrt(16)  # Confusing!

Best Practice: Import specific names or use the basic import statement. Avoid from module import * except in interactive sessions where you're experimenting.

Renaming Imports with as

Sometimes module or function names are long, or you want to avoid name conflicts. The as keyword lets you create an alias:

python
import math as m
 
result = m.sqrt(36)
print(result)  # Output: 6.0

This is particularly useful for modules with long names or when following common conventions:

python
import datetime as dt
 
today = dt.date.today()
print(today)  # Output: 2025-12-19 (or current date)

You can also rename specific imports:

python
from math import sqrt as square_root
 
result = square_root(49)
print(result)  # Output: 7.0

This is helpful when you have name conflicts:

python
from math import sqrt as math_sqrt
 
def sqrt(x):
    """Custom square root with input validation."""
    if x < 0:
        return None
    return math_sqrt(x)
 
print(sqrt(25))   # Output: 5.0 (your function)
print(sqrt(-4))   # Output: None (your function)

Combining Import Styles

You can mix different import styles in the same file:

python
import math
from datetime import date, time
from random import randint as random_int
 
# Use math with prefix
radius = 5
area = math.pi * radius ** 2
 
# Use date and time directly
today = date.today()
current_time = time(14, 30)
 
# Use renamed function
dice_roll = random_int(1, 6)

Choosing the Right Import Style

Here's a decision guide:

Use import module when:

  • You need multiple items from the module
  • You want maximum clarity about where names come from
  • The module name is short and clear

Use from module import name when:

  • You need only one or two specific items
  • The names are distinctive and unlikely to conflict
  • You'll use the names frequently

Use import module as alias when:

  • The module name is very long
  • Following a common convention (like import numpy as np)
  • You need to avoid conflicts with other modules

Avoid from module import * in production code:

  • Use it only for quick experiments in the interactive shell
  • Never use it in modules that others will import

Let's see a complete example demonstrating good import practices:

python
# data_processor.py
import math
from statistics import mean, median
from datetime import datetime as dt
 
def calculate_statistics(numbers):
    """Calculate various statistics for a list of numbers."""
    if not numbers:
        return None
    
    avg = mean(numbers)
    mid = median(numbers)
    std_dev = math.sqrt(sum((x - avg) ** 2 for x in numbers) / len(numbers))
    
    return {
        'mean': avg,
        'median': mid,
        'std_dev': std_dev,
        'timestamp': dt.now()
    }
 
# Test the function
data = [10, 20, 30, 40, 50]
stats = calculate_statistics(data)
print(f"Mean: {stats['mean']}")      # Output: Mean: 30.0
print(f"Median: {stats['median']}")  # Output: Median: 30
print(f"Std Dev: {stats['std_dev']:.2f}")  # Output: Std Dev: 14.14

This example shows:

  • import math for the full module (we might use other math functions later)
  • from statistics import mean, median for specific functions we use frequently
  • from datetime import datetime as dt for a commonly aliased module

22.3) Overview of Common Python Standard Library Modules

Python comes with a rich standard library—a collection of modules that provide solutions to common programming tasks. These modules are always available; you don't need to install anything extra. Understanding what's available in the standard library helps you avoid "reinventing the wheel."

The math Module

The math module provides mathematical functions beyond basic arithmetic:

python
import math
 
# Trigonometric functions
angle_rad = math.radians(45)  # Convert degrees to radians
print(math.sin(angle_rad))    # Output: 0.7071067811865476
print(math.cos(angle_rad))    # Output: 0.7071067811865475
 
# Rounding and absolute value
print(math.ceil(4.2))   # Output: 5 (round up)
print(math.floor(4.8))  # Output: 4 (round down)
print(math.fabs(-7.5))  # Output: 7.5 (absolute value as float)
 
# Exponential and logarithmic
print(math.exp(2))      # Output: 7.38905609893065 (e^2)
print(math.log(100))    # Output: 4.605170185988092 (natural log)
print(math.log10(100))  # Output: 2.0 (base-10 log)
 
# Constants
print(math.pi)  # Output: 3.141592653589793
print(math.e)   # Output: 2.718281828459045

As we learned in Chapter 4, the math module is essential for advanced mathematical operations.

The random Module

The random module generates pseudo-random numbers and makes random selections:

python
import random
 
# Random integers
dice = random.randint(1, 6)  # Random integer from 1 to 6 (inclusive)
print(f"Dice roll: {dice}")
 
# Random floats
probability = random.random()  # Random float from 0.0 to 1.0
print(f"Probability: {probability:.4f}")
 
# Random choice from a sequence
colors = ['red', 'blue', 'green', 'yellow']
chosen_color = random.choice(colors)
print(f"Chosen color: {chosen_color}")
 
# Shuffle a list in place
deck = ['A', 'K', 'Q', 'J', '10']
random.shuffle(deck)
print(f"Shuffled deck: {deck}")
 
# Random sample without replacement
lottery_numbers = random.sample(range(1, 50), 6)
print(f"Lottery numbers: {sorted(lottery_numbers)}")

Output (example - will vary due to randomness):

Dice roll: 4
Probability: 0.7382
Chosen color: green
Shuffled deck: ['Q', 'A', '10', 'K', 'J']
Lottery numbers: [7, 15, 23, 31, 38, 42]

The datetime Module

The datetime module handles dates and times:

python
from datetime import date, time, datetime, timedelta
 
# Current date and time
today = date.today()
now = datetime.now()
print(f"Today: {today}")  # Output: Today: 2025-12-19
print(f"Now: {now}")      # Output: Now: 2025-12-19 14:30:45.123456
 
# Creating specific dates and times
birthday = date(1990, 5, 15)
meeting_time = time(14, 30)
appointment = datetime(2025, 12, 25, 10, 0)
 
print(f"Birthday: {birthday}")          # Output: Birthday: 1990-05-15
print(f"Meeting: {meeting_time}")       # Output: Meeting: 14:30:00
print(f"Appointment: {appointment}")    # Output: Appointment: 2025-12-25 10:00:00
 
# Date arithmetic with timedelta
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
print(f"Tomorrow: {tomorrow}")    # Output: Tomorrow: 2025-12-20
print(f"Next week: {next_week}")  # Output: Next week: 2025-12-26
 
# Extracting components
print(f"Year: {today.year}")      # Output: Year: 2025
print(f"Month: {today.month}")    # Output: Month: 12
print(f"Day: {today.day}")        # Output: Day: 19

The os Module

The os module provides operating system functionality. We'll explore this in detail in Chapter 26, but here's a preview:

python
import os
 
# Current working directory
current_dir = os.getcwd()
print(f"Current directory: {current_dir}")
 
# List files in a directory
files = os.listdir('.')
print(f"Files: {files[:3]}")  # Show first 3 files
 
# Check if a path exists
exists = os.path.exists('myfile.txt')
print(f"File exists: {exists}")
 
# Join path components (works across operating systems)
file_path = os.path.join('data', 'users', 'profile.txt')
print(f"Path: {file_path}")  # Output: data/users/profile.txt (or data\users\profile.txt on Windows)

The sys Module

The sys module provides system-specific parameters and functions:

python
import sys
 
# Python version information
print(f"Python version: {sys.version}")
print(f"Version info: {sys.version_info}")
 
# Platform information
print(f"Platform: {sys.platform}")  # Output: linux, darwin, win32, etc.
 
# Maximum integer size
print(f"Max int: {sys.maxsize}")

The statistics Module

The statistics module provides functions for statistical calculations:

python
import statistics
 
grades = [85, 92, 78, 90, 88, 95, 82]
 
# Central tendency
avg = statistics.mean(grades)
mid = statistics.median(grades)
mode_val = statistics.mode([1, 2, 2, 3, 3, 3, 4])
 
print(f"Mean: {avg}")      # Output: Mean: 87.14285714285714
print(f"Median: {mid}")    # Output: Median: 88
print(f"Mode: {mode_val}") # Output: Mode: 3
 
# Spread
std_dev = statistics.stdev(grades)
variance = statistics.variance(grades)
 
print(f"Standard deviation: {std_dev:.2f}")  # Output: Standard deviation: 5.90
print(f"Variance: {variance:.2f}")           # Output: Variance: 34.81

The collections Module

The collections module provides specialized container types. We'll explore this more in Chapter 39, but here's a taste:

python
from collections import Counter, defaultdict
 
# Counter - count occurrences
text = "hello world"
letter_counts = Counter(text)
print(letter_counts)  # Output: Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})
print(letter_counts['l'])  # Output: 3
 
# defaultdict - dictionary with default values
word_lists = defaultdict(list)
word_lists['fruits'].append('apple')
word_lists['fruits'].append('banana')
word_lists['vegetables'].append('carrot')
print(dict(word_lists))  # Output: {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']}

Finding More Standard Library Modules

Python's standard library contains over 200 modules. You can explore them in several ways:

python
# See all available modules (this takes a moment)
help('modules')
 
# Get help on a specific module
import math
help(math)
 
# See what's in a module
import random
print(dir(random))

The Python documentation (https://docs.python.org/3/library/) provides comprehensive information about every standard library module. As you gain experience, you'll discover which modules are most useful for your work.

22.4) Creating and Using Your Own Modules

Creating your own modules is straightforward—any Python file can be a module. The key is organizing your code thoughtfully so modules are focused, reusable, and easy to understand.

Creating a Simple Module

Let's create a module for working with student grades. Create a file called grade_calculator.py:

python
# grade_calculator.py
"""Module for calculating and analyzing student grades."""
 
def calculate_average(grades):
    """Calculate the average of a list of grades."""
    if not grades:
        return 0
    return sum(grades) / len(grades)
 
def get_letter_grade(numeric_grade):
    """Convert a numeric grade to a letter grade."""
    if numeric_grade >= 90:
        return 'A'
    elif numeric_grade >= 80:
        return 'B'
    elif numeric_grade >= 70:
        return 'C'
    elif numeric_grade >= 60:
        return 'D'
    else:
        return 'F'
 
def find_highest(grades):
    """Find the highest grade in a list."""
    if not grades:
        return None
    return max(grades)
 
def find_lowest(grades):
    """Find the lowest grade in a list."""
    if not grades:
        return None
    return min(grades)
 
# Module-level constants
PASSING_GRADE = 60
HONOR_ROLL_THRESHOLD = 90

Now create another file to use this module:

python
# student_report.py
import grade_calculator
 
# Student's test scores
test_scores = [85, 92, 78, 88, 95]
 
# Calculate statistics
average = grade_calculator.calculate_average(test_scores)
letter = grade_calculator.get_letter_grade(average)
highest = grade_calculator.find_highest(test_scores)
lowest = grade_calculator.find_lowest(test_scores)
 
# Generate report
print("Student Grade Report")
print("=" * 40)
print(f"Test Scores: {test_scores}")
print(f"Average: {average:.1f}")
print(f"Letter Grade: {letter}")
print(f"Highest Score: {highest}")
print(f"Lowest Score: {lowest}")
 
# Check for honor roll
if average >= grade_calculator.HONOR_ROLL_THRESHOLD:
    print("Status: HONOR ROLL!")
elif average >= grade_calculator.PASSING_GRADE:
    print("Status: Passing")
else:
    print("Status: Needs Improvement")

Output:

Student Grade Report
========================================
Test Scores: [85, 92, 78, 88, 95]
Average: 87.6
Letter Grade: B
Highest Score: 95
Lowest Score: 78
Status: Passing

Module Documentation

Notice the docstring at the top of grade_calculator.py. This module-level docstring describes what the module does. It appears when someone uses help():

python
import grade_calculator
help(grade_calculator)

This displays the module documentation, including the module docstring and all function docstrings. Good documentation makes your modules easier to use.

Module-Level Variables and Constants

Modules can contain variables that are shared across all uses of the module. These are often used for configuration or constants:

python
# config.py
"""Application configuration settings."""
 
# Database settings
DB_HOST = "localhost"
DB_PORT = 5432
DB_NAME = "myapp"
 
# Application settings
MAX_LOGIN_ATTEMPTS = 3
SESSION_TIMEOUT = 1800  # seconds
DEBUG_MODE = False
 
# File paths
DATA_DIR = "/var/data"
LOG_DIR = "/var/log"
 
# Feature flags
ENABLE_CACHING = True
ENABLE_LOGGING = True

Using configuration from a module:

python
# app.py
import config
 
def connect_database():
    """Connect to the database using config settings."""
    print(f"Connecting to {config.DB_HOST}:{config.DB_PORT}")
    print(f"Database: {config.DB_NAME}")
    
    if config.DEBUG_MODE:
        print("DEBUG: Connection details logged")
 
def check_login_attempts(attempts):
    """Check if login attempts exceed the limit."""
    if attempts >= config.MAX_LOGIN_ATTEMPTS:
        print(f"Too many attempts! Maximum is {config.MAX_LOGIN_ATTEMPTS}")
        return False
    return True
 
connect_database()
print(check_login_attempts(2))  # Output: True
print(check_login_attempts(4))  # Output: Too many attempts! Maximum is 3

Output:

Connecting to localhost:5432
Database: myapp
True
Too many attempts! Maximum is 3

Important: Module-level variables are shared across all imports. If you modify a module variable, the change affects all code using that module:

python
# file1.py
import config
config.DEBUG_MODE = True
print(f"File1 - Debug mode: {config.DEBUG_MODE}")
 
# file2.py
import config
print(f"File2 - Debug mode: {config.DEBUG_MODE}")  # Will be True!

This behavior can be useful but also surprising. Be cautious when modifying module-level variables.

Private Names in Modules

By convention, names starting with an underscore are considered private or internal to the module:

python
# user_manager.py
"""Module for managing user accounts."""
 
# Private helper function
def _validate_email(email):
    """Internal function to validate email format."""
    return '@' in email and '.' in email
 
# Public function
def create_user(username, email):
    """Create a new user account."""
    if not _validate_email(email):
        return None
    
    user = {
        'username': username,
        'email': email,
        'active': True
    }
    return user
 
# Private constant
_MAX_USERNAME_LENGTH = 20
 
# Public constant
MIN_PASSWORD_LENGTH = 8

When you use from user_manager import *, private names (those starting with underscore) are not imported. However, you can still access them explicitly if needed:

python
import user_manager
 
# Public function - intended for use
user = user_manager.create_user("alice", "alice@example.com")
 
# Private function - can access but shouldn't rely on it
# (it might change in future versions)
is_valid = user_manager._validate_email("test@test.com")

The underscore prefix is a signal to other programmers: "This is an implementation detail. Don't depend on it staying the same."

22.5) Understanding Packages and __init__.py

As projects grow, you'll want to organize multiple related modules into a package. A package is a directory containing Python modules and a special __init__.py file.

What Is a Package?

A package is a way to organize multiple modules into a hierarchical structure. Think of it as a folder that contains Python files, where the folder itself can be imported.

Here's a simple package structure:

myproject/
    main.py
    utilities/
        __init__.py
        text.py
        math.py
        file.py

In this structure, utilities is a package containing three modules: text, math, and file. The __init__.py file (which can be empty) tells Python that utilities is a package.

Creating a Simple Package

Let's create a package for data processing. First, create this directory structure:

data_tools/
    __init__.py
    validators.py
    formatters.py

Create validators.py:

python
# data_tools/validators.py
"""Data validation functions."""
 
def is_valid_email(email):
    """Check if email has basic valid format."""
    return '@' in email and '.' in email.split('@')[1]
 
def is_valid_phone(phone):
    """Check if phone number has valid format (simple check)."""
    digits = ''.join(c for c in phone if c.isdigit())
    return len(digits) == 10
 
def is_positive_number(value):
    """Check if value is a positive number."""
    try:
        return float(value) > 0
    except (ValueError, TypeError):
        return False

Create formatters.py:

python
# data_tools/formatters.py
"""Data formatting functions."""
 
def format_phone(phone):
    """Format phone number as (XXX) XXX-XXXX."""
    digits = ''.join(c for c in phone if c.isdigit())
    if len(digits) != 10:
        return phone
    return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
 
def format_currency(amount):
    """Format number as currency."""
    return f"${amount:,.2f}"
 
def format_percentage(value, decimals=1):
    """Format number as percentage."""
    return f"{value * 100:.{decimals}f}%"

Create an empty __init__.py:

python
# data_tools/__init__.py
"""Data processing tools package."""

Importing from Packages

Now you can import from the package in several ways:

python
# Method 1: Import the module from the package
import data_tools.validators
 
email = "user@example.com"
is_valid = data_tools.validators.is_valid_email(email)
print(f"Email valid: {is_valid}")  # Output: Email valid: True
 
# Method 2: Import specific module with from
from data_tools import formatters
 
phone = "1234567890"
formatted = formatters.format_phone(phone)
print(f"Formatted phone: {formatted}")  # Output: Formatted phone: (123) 456-7890
 
# Method 3: Import specific functions
from data_tools.validators import is_valid_phone
from data_tools.formatters import format_currency
 
print(is_valid_phone("555-1234"))  # Output: False (not 10 digits)
print(format_currency(1234.56))    # Output: $1,234.56

The __init__.py File

The __init__.py file serves two purposes:

  1. Marks the directory as a package: Python recognizes directories with __init__.py as packages
  2. Package initialization code: Code in __init__.py runs when the package is first imported

The __init__.py file can be empty, but it can also contain code to make the package easier to use:

python
# data_tools/__init__.py
"""Data processing tools package."""
 
# Import commonly used functions into package namespace
from data_tools.validators import is_valid_email, is_valid_phone
from data_tools.formatters import format_phone, format_currency
 
# Package version
__version__ = '1.0.0'
 
# Package-level constant
DEFAULT_CURRENCY_SYMBOL = '$'

Now users can import directly from the package:

python
# Instead of: from data_tools.validators import is_valid_email
# You can write:
from data_tools import is_valid_email, format_currency
 
print(is_valid_email("test@test.com"))  # Output: True
print(format_currency(99.99))           # Output: $99.99

22.6) The __name__ Variable and if __name__ == "__main__": Running a File as a Script

Python files can serve two purposes: they can be imported as modules or run as standalone scripts. The special __name__ variable helps you write code that works well in both situations.

Understanding __name__

Every Python module has a built-in variable called __name__. Python sets this variable differently depending on how the file is being used:

  • When imported: __name__ is set to the module's name
  • When run directly: __name__ is set to "__main__"

Let's see this in action. Create a file called demo_name.py:

python
# demo_name.py
print(f"The __name__ variable is: {__name__}")

Now run it directly:

bash
python demo_name.py

Output:

The __name__ variable is: __main__

Now import it from another file:

python
# test_import.py
import demo_name

Output:

The __name__ variable is: demo_name

When you run demo_name.py directly, Python sets __name__ to "__main__". When you import it, Python sets __name__ to the module name ("demo_name").

The if __name__ == "__main__": Pattern

This behavior lets you write code that only runs when the file is executed directly, not when it's imported. This is done with the pattern:

python
if __name__ == "__main__":
    # Code here runs only when file is executed directly
    pass

Here's why this is useful. Create math_utils.py:

python
# math_utils.py
"""Utility functions for mathematical operations."""
 
def calculate_area(radius):
    """Calculate the area of a circle."""
    return 3.14159 * radius ** 2
 
def calculate_circumference(radius):
    """Calculate the circumference of a circle."""
    return 2 * 3.14159 * radius
 
# Test code - runs only when file is executed directly
if __name__ == "__main__":
    print("Testing math_utils functions...")
    
    test_radius = 5
    area = calculate_area(test_radius)
    circumference = calculate_circumference(test_radius)
    
    print(f"Radius: {test_radius}")
    print(f"Area: {area:.2f}")
    print(f"Circumference: {circumference:.2f}")

When you run this file directly:

bash
python math_utils.py

Output:

Testing math_utils functions...
Radius: 5
Area: 78.54
Circumference: 31.42

But when you import it:

python
# use_math_utils.py
import math_utils
# The test code doesn't run!
 
area = math_utils.calculate_area(10)
print(f"Area of circle: {area:.2f}")  # Output: Area of circle: 314.16

The test code in the if __name__ == "__main__": block doesn't execute during import. This lets you include test code, examples, or demonstrations in your modules without affecting code that imports them.

Common Uses of if __name__ == "__main__":

Testing and Demonstrations

Include examples showing how to use your module:

python
# string_tools.py
def reverse_string(text):
    """Reverse a string."""
    return text[::-1]
 
def count_vowels(text):
    """Count vowels in text."""
    vowels = 'aeiouAEIOU'
    return sum(1 for char in text if char in vowels)
 
if __name__ == "__main__":
    # Demonstration code
    sample = "Hello, World!"
    
    print(f"Original: {sample}")
    print(f"Reversed: {reverse_string(sample)}")
    print(f"Vowels: {count_vowels(sample)}")

In this chapter, we've learned how to organize Python code using modules and packages. We explored how the import system works, different ways to import code, and how to create our own modules and packages. We also learned about the __name__ variable and the if __name__ == "__main__": pattern that lets files work both as importable modules and standalone scripts.

These organizational tools become increasingly important as your programs grow. In the next chapter, we'll explore how to use functions as data and apply simple functional programming techniques, building on the solid foundation of code organization we've established here.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai