Python Tutorials

Create Custom Python Modules : Structure Large Projects

When you first start learning Python, it is entirely normal to write all of your code in a single file named main.py. However, as you transition from writing simple scripts to building complex applications, that single file will quickly grow to thousands of lines. It will become impossible to navigate, incredibly difficult to debug, and a nightmare to collaborate on with other developers.

To solve this, professional developers break their code down into multiple, manageable files called Custom Modules. By structuring your project with custom modules, you can separate your database logic from your user interface, reuse the same utility functions across entirely different projects, and keep your codebase clean, organized, and scalable.

What is Custom Modules?

In Python, a Custom Module is simply a text file containing Python code (with a .py extension) that you have written yourself, which is intended to be imported and used by another Python file.

When you group multiple related custom modules together inside a folder, that folder is officially known as a Package. Structuring larger Python projects involves architecting a logical hierarchy of these modules and packages.

Syntax & Basic Usage

Creating a custom module requires absolutely no special syntax. You simply create a new Python file, write your functions or variables inside it, and then use the import keyword in your main script to pull that code in. Both files must be saved in the same directory for this basic setup to work.

# --- FILE 1: mathematics.py (This is our custom module) ---
def add_numbers(num1, num2):
    return num1 + num2

pi_value = 3.14159


# --- FILE 2: main_script.py (This is our primary application) ---
# We import our custom module by its filename (without the .py)
import mathematics

# We access functions and variables using dot notation
total_sum = mathematics.add_numbers(10, 25)
circle_constant = mathematics.pi_value

print(f"The total sum is: {total_sum}")
print(f"The value of Pi is: {circle_constant}")

# Expected Output (when running main_script.py):
# The total sum is: 35
# The value of Pi is: 3.14159

Code language: PHP (php)

Importing Functions and Passing Arguments in Custom Python Modules

Let’s dive deeper into how to import specific pieces of your custom modules, how to group them into packages, and how to use Python’s most famous built-in module protection feature.

1. Specific Imports and Aliasing

If your custom module has fifty functions, but your main script only needs one, importing the entire module wastes memory and forces you to type the module name repeatedly. Instead, you can import exactly what you need.

# --- FILE 1: string_utilities.py ---
def make_uppercase(text):
    return text.upper()

def make_lowercase(text):
    return text.lower()


# --- FILE 2: app.py ---
# Import ONLY the specific function we need, and use 'as' to give it a short nickname
from string_utilities import make_uppercase as shout

# Now we can use the nickname directly without the module prefix!
loud_greeting = shout("hello world")

print(loud_greeting)

# Expected Output (when running app.py):
# HELLO WORLD

Code language: PHP (php)

2. Creating Packages and __init__.py

When you have dozens of modules, you should group them into folders (Packages). To tell Python that a normal folder should be treated as an official Python Package, you must place a special file inside it named __init__.py.

(Note: In modern Python 3.3+, this file can sometimes be omitted, but including it is the strict industry standard and allows you to run initialization code).

# --- PROJECT STRUCTURE ---
# /my_project
# ├── main.py
# └── /ecommerce (This is our Package)
#     ├── __init__.py (Can be completely empty)
#     └── billing.py (Our custom module inside the package)

# --- FILE: ecommerce/billing.py ---
def calculate_tax(subtotal):
    return subtotal * 0.08


# --- FILE: main.py ---
# We import using dot notation: folder_name.module_name
from ecommerce.billing import calculate_tax

final_tax = calculate_tax(100.00)
print(f"Tax to pay: ${final_tax}")

# Expected Output (when running main.py):
# Tax to pay: $8.0

Code language: PHP (php)

3. The __all__ Variable (Controlling import *)

Sometimes developers use from custom_module import * to blindly import everything from a file. You can restrict exactly what gets imported by defining an __all__ list inside your custom module.

# --- FILE: secure_database.py ---
# We only allow 'public_query' to be exported. The secret password is hidden!
__all__ = ["public_query"]

secret_database_password = "SuperSecret123"

def public_query():
    return "Fetching public records..."


# --- FILE: app.py ---
# The * will ONLY import what is listed in the __all__ array
from secure_database import *

print(public_query())

# ❌ THIS WOULD CAUSE AN ERROR:
# print(secret_database_password) 
# NameError: name 'secret_database_password' is not defined

# Expected Output (when running app.py):
# Fetching public records...

Code language: PHP (php)

4. The Magic of if __name__ == "__main__":

This is arguably the most important concept when building custom modules.

When you import a custom module, Python physically reads and executes every single line of code in that file from top to bottom. If your custom module contains test print() statements at the bottom, they will accidentally print out in your main application!

To prevent this, you wrap your test code inside if __name__ == "__main__":. This block tells Python: “Only run this code if I am executing this specific file directly. If this file is being imported by someone else, ignore this block entirely.”

# --- FILE: text_cleaner.py ---
def remove_spaces(text_string):
    return text_string.replace(" ", "")

# This block protects our test code from running during an import
if __name__ == "__main__":
    # This only prints if we explicitly run 'python text_cleaner.py'
    print("--- Running Internal Module Tests ---")
    test_result = remove_spaces("A B C")
    print(f"Test Result: {test_result}")


# --- FILE: main.py ---
import text_cleaner

# Because of the if __name__ == "__main__" block in the other file, 
# importing it does NOT print the internal tests!
clean_word = text_cleaner.remove_spaces("Hello Python")
print(f"Main App Output: {clean_word}")

# Expected Output (when running main.py):
# Main App Output: HelloPython

Code language: PHP (php)

Real-World Practical Examples

Scenario 1: A Modular E-Commerce Checkout System

Instead of putting user input, mathematical logic, and receipt generation all in one massive file, professional developers separate the mathematical logic into a “utility” module.

# --- FILE 1: tax_utilities.py (Handles strict math logic) ---
def apply_regional_tax(cart_total, region_code):
    tax_rates = {
        "NY": 0.08,
        "CA": 0.07,
        "TX": 0.06
    }
    # Use .get() to default to 0 if the region isn't found
    regional_rate = tax_rates.get(region_code, 0.0)
    tax_amount = cart_total * regional_rate
    return cart_total + tax_amount


# --- FILE 2: store_front.py (Handles the user interaction) ---
import tax_utilities

def process_customer_checkout(customer_cart_value, customer_state):
    print("Initiating checkout process...")
    
    # We rely on our custom module to do the heavy lifting
    final_amount = tax_utilities.apply_regional_tax(customer_cart_value, customer_state)
    
    print(f"Total charged to credit card: ${final_amount:.2f}")

# Simulating a user checkout
process_customer_checkout(250.00, "NY")

# Expected Output (when running store_front.py):
# Initiating checkout process...
# Total charged to credit card: $270.00

Code language: PHP (php)

Scenario 2: Data Processing Pipeline

When scraping data from the web, the data is usually incredibly messy. We can build a dedicated “cleaner” module to keep our main scraping script readable.

# --- FILE 1: data_cleaner.py ---
def format_currency(raw_string):
    """Removes spaces, dollar signs, and converts to a float."""
    cleaned_string = raw_string.replace("$", "").replace(" ", "")
    return float(cleaned_string)

def format_title(raw_title):
    """Removes extra spaces and capitalizes the first letter."""
    return raw_title.strip().title()


# --- FILE 2: scraper_app.py ---
import data_cleaner

raw_product_title = "   vintage leather jacket   "
raw_product_price = " $ 150.50 "

# Utilizing our custom module tools
clean_title = data_cleaner.format_title(raw_product_title)
clean_price = data_cleaner.format_currency(raw_product_price)

print(f"Product: {clean_title} | Price: ${clean_price}")

# Expected Output (when running scraper_app.py):
# Product: Vintage Leather Jacket | Price: $150.5

Code language: PHP (php)

Best Practices & Common Pitfalls

  • The Shadowing Trap (Naming Conflicts): Never name your custom module the exact same name as a built-in Python module. If you create a file named math.py or random.py, your program will accidentally import your custom file instead of the official Python library, causing disastrous crashes across your application.
  • Circular Imports: A circular import happens when Module A tries to import Module B, but Module B is simultaneously trying to import Module A. Python gets trapped in an infinite loop and crashes. Always structure your project so that utility modules do not import the main application files.
  • Use __init__.py for Package Setup: While you can leave __init__.py empty, professional developers use it to expose exactly what the package should offer. You can import specific functions into the __init__.py file so that other developers can import them directly from the package name, rather than drilling down into the sub-modules.
  • Keep Modules Highly Cohesive: A custom module should focus on one specific theme. A database.py module should only contain functions that talk to the database. It should not contain functions that format dates or calculate taxes.

Summary

  • Custom Modules are simply Python (.py) files you write to organize and separate your code logically.
  • Packages are directories containing multiple related modules, officially designated by the presence of an __init__.py file.
  • You can import specific functions using from module import function, and rename them on the fly using as.
  • The __all__ list allows you to secure your module by restricting what gets exported when another file uses import *.
  • Always place test code inside your custom modules within an if __name__ == "__main__": block to prevent it from executing automatically when imported.

Leave a Comment