The right import structure prevents naming conflicts and makes your code readable
In a large Python project, imports become a source of confusion and bugs if you do not organize them deliberately. The problem is not imports themselves — it is that as your project grows, you end up with circular dependencies, unclear where a function actually lives, and modules that load slower than they should. The solution is to structure your imports in a consistent way from the start, use relative imports where they belong, and keep your package organization clear enough that another developer (or you in six months) can find what they need.
This guide walks you through the actual patterns that work in real projects: how to lay out your directories, when to use relative versus absolute imports, how to handle circular dependencies when they happen, and how to organize your imports so they are readable at a glance.
Key Takeaways
- Organize your project into packages (directories with __init__.py files) so Python knows where to find modules, and use absolute imports from your project root in most cases.
- Group your imports in three sections — standard library, third-party packages, and your own code — separated by blank lines so readers can scan them quickly.
- Use relative imports only within a package when you are importing from sibling or parent modules, and avoid them across package boundaries.
- Circular imports happen when module A imports from module B and module B imports from module A; move the import inside a function or restructure your code so one module does not depend on the other.
- Use __init__.py files to expose your package's public interface, so users of your package import from one clear location instead of hunting through submodules.
Set up your project structure so imports work consistently
Python looks for modules in a specific order: first in the directory where the script runs, then in directories listed in your PYTHONPATH, then in the standard library. In a large project, you control this by creating a clear directory structure and making sure your project root is where Python starts looking.
Create a top-level directory for your project. Inside it, create subdirectories for different parts of your code, and put an __init__.py file in each one. That __init__.py can be empty, but its presence tells Python "this directory is a package." Here is what a real project structure looks like:
myproject/ myproject/ __init__.py core/ __init__.py database.py models.py utils/ __init__.py helpers.py validators.py api/ __init__.py routes.py tests/ __init__.py test_core.py setup.py requirements.txt
The outer myproject directory is your project root. The inner myproject directory is your actual package. When you run code or tests from the project root, Python can find everything inside the inner myproject package by name. This structure lets you write imports like from myproject.core import database from anywhere in your project, and they will work the same way.
Use absolute imports as your default, organized in three groups
An absolute import names the full path from your project root. Instead of from . import helpers, you write from myproject.utils import helpers. Absolute imports are longer to type, but they are clearer, they work the same way no matter where the file is, and they are what most Python projects use.
At the top of every file, group your imports in this order: standard library modules first, then third-party packages, then your own code. Separate each group with a blank line. Here is what it looks like in practice:
import os import sys from datetime import datetime import requests import numpy as np from flask import Flask, jsonify from myproject.core import database from myproject.utils import validators from myproject.api import routes
This layout makes it obvious what each import does. A reader scanning the file knows immediately what external dependencies the code needs, and what internal modules it relies on. Tools like isort can sort your imports automatically if you run them through the command line, which saves time in large projects where many files need the same treatment.
Use relative imports only within a single package
A relative import uses dots to refer to the current package or parent packages. from . import helpers means "import helpers from the same directory as this file." from .. import core means "go up one level and import core." Relative imports are useful when you are moving code around inside a package and do not want to update every import path, but they only work within a package, and they can be confusing to read.
Use relative imports only when you are importing from sibling modules or from a parent package, and only when those modules are part of the same logical package. For example, in the myproject.utils package, if validators.py needs something from helpers.py, you can write from . import helpers because both files are in the same utils directory. If validators.py needs something from myproject.core, use an absolute import: from myproject.core import database.
Do not use relative imports across package boundaries. If your code is in myproject.api and needs something from myproject.core, do not write from ..core import models. Write from myproject.core import models instead. Absolute imports are clearer and they work the same way everywhere in your project.
Fix circular imports by moving the import inside a function
A circular import happens when module A imports from module B, and module B imports from module A. Python loads modules top-to-bottom, so when A tries to import from B, B is not fully loaded yet, and you get an ImportError or an attribute that does not exist.
Here is a real example. Suppose myproject/core/models.py imports a function from myproject/core/database.py, and database.py imports a class from models.py:
# myproject/core/models.py from myproject.core.database import get_connection class User: def save(self): conn = get_connection() # myproject/core/database.py from myproject.core.models import User def get_connection(): return None
When Python tries to load models.py, it hits the import statement and tries to load database.py. But database.py immediately tries to import User from models.py, which is not finished loading yet. The import fails.
The fix is to move the import inside the function that actually uses it. That way, the import does not run until the function is called, by which time both modules are fully loaded:
# myproject/core/database.py def get_connection(): from myproject.core.models import User return None
This works, but it is a sign that your code structure needs rethinking. If two modules depend on each other, consider whether one of them should be split into smaller pieces, or whether the shared code should move to a third module that both can import from without creating a cycle.
Use __init__.py to define what your package exports
The __init__.py file in a package directory runs when someone imports from that package. You can leave it empty, but in a large project it is useful to put your package's public interface there. This way, users of your package know what to import and where to find it.
For example, suppose myproject/core has two modules: database.py and models.py. You want users to import the User class and the get_connection function, but you do not want them hunting through submodules. Put this in myproject/core/__init__.py:
from myproject.core.database import get_connection from myproject.core.models import User __all__ = ['get_connection', 'User']
Now a user can write from myproject.core import User, get_connection instead of from myproject.core.models import User and from myproject.core.database import get_connection. The __all__ list tells Python and documentation tools what is part of the public interface. This makes your package easier to use and gives you flexibility to reorganize the internal structure later without breaking code that depends on you.
Handle imports in tests by running from your project root
Test files often struggle with imports because they live in a separate directory. If you run a test file directly with python tests/test_core.py, Python adds the tests directory to the path, not your project root, and imports fail.
The solution is to run tests from your project root using a test runner. Use pytest (the most common choice) or unittest (built into Python). From your project root, run pytest tests/ or python -m unittest discover. The test runner adds your project root to the path, so imports like from myproject.core import database work in your test files the same way they work in your main code.
If you must run a test file directly, add this at the very top before any imports:
import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent))
This adds your project root to the path. But this is a workaround — use a test runner instead, because it handles this automatically and is how your code will actually run in production.
Frequently Asked Questions
What is the difference between from X import Y and import X?
import X loads the entire module and you access things inside it with dot notation: import os; os.path.exists(). from X import Y loads only Y from module X and you use it directly: from os.path import exists; exists(). Use from X import Y when you only need one or two things and want cleaner code. Use import X when you need many things from a module or when the module name is short and clear.
Should I use star imports like from myproject.core import *?
No. Star imports make it unclear what you are actually using, they can cause naming conflicts if two modules export something with the same name, and they make your code harder to understand. Always import the specific things you need by name. The only exception is in __init__.py files where you are deliberately re-exporting a package's public interface.
Why do I get ImportError when I try to import my own code?
Python cannot find your module because your project root is not in the path. Make sure you are running your code from the project root directory, not from a subdirectory. If you are running tests, use a test runner like pytest instead of running the test file directly. If you are running a script, run it with python -m myproject.script_name instead of python myproject/script_name.py.
Can I use relative imports across different packages?
No. Relative imports only work within a single package. If you are in myproject.api and need something from myproject.core, use an absolute import: from myproject.core import models. Relative imports across packages are confusing and often fail in unexpected ways.
What does __pycache__ do and should I commit it to version control?
__pycache__ is a directory Python creates to store compiled bytecode of your modules, which makes them load faster on the next run. You should not commit it to version control — add __pycache__/ to your .gitignore file. Python recreates it automatically when needed, and it is specific to your machine and Python version.