Last Updated: June 01, 2026
- What’s the difference between a python package vs module
- What happens when you import a python module
- How do you make a package in your python project
- Only Import a part of a module
- Importing a module and applying an alias
- Importing modules outside your project folder
- How to import modules dynamically
- Conclusion
- Get notified automatically of new articles
- Related Articles
- Frequently Asked Questions
Beginner
Importing modules or packages (in other languages this would be referred to as libraries) is a fundamental aspect of the language which makes it so useful. As of this writing, the most popular python package library, pypi.org, has over 300k packages to import. This isn’t just important for importing of external packages. It also becomes a must when your own project becomes quite large. You need to make sure you can split your code into manageable logical chunks which can talk to each other. This is what this article is all about.
Python developer and educator with 15+ years building production systems across data engineering, web APIs, and AI tooling. Founder of Python How To Program — 270+ in-depth tutorials covering the modern Python stack.
What’s the difference between a python package vs module
First, some terminology. A module, is a single python file (still with a .py extension) that contains some code which you can import. While a package, is a collection of files. In your project, a package is all the files in a given directory and where the directory also contains the file __init__.py to signal that this is a package.
What happens when you import a python module
There is nothing special in fact you need to do to make a module – all python files are by default a module and can be imported. When a file is imported, all the code does get processed – e.g. if there’s any code to be executed it will run.
See following example. Suppose we have the following relationship:

Code as follows:
#module1.py
print("module1: I'm in module 1 root section")
def output_hw():
print("module1: Hello world - output_hw 1")
#module2.py
import module1
print("module2: I'm in root section of module 2")
def output_hw():
print("module2: Hello world - output_hw 2")
#main_file.py
print("main_file: starting code")
import module1
import module2
print("main_file: I'm in the root section ")
if __name__ == '__main__':
print("main_file: ******* starting __main__ section")
module1.output_hw()
module2.output_hw()
print("main_file: Main file done!")
Output:

So what’s happening here:
- The main_file.py gets executed first and then imports module1 then module2
- As part of importing
module1, it executes all the code including the print statements in the root part of the code. Similarly formodule2 - Then the code returns to the main_file where it calls the functions under module1 and
module2. - Please note, that both
module1andmodule2have the same function name ofoutput_hw(). This is perfectly fine as the scope of the function is in different modules.
One additional item to note, is that the module2 also imports module1. However, the print statement in the root section print("module1: I'm in module 1 root section") did not get executed the second time. Why? Python only imports a given module once.
Now let’s make a slight change – let’s remove the references to module1 in the main_file, and in module2, import module1!

The updated code looks like this:
#module1.py
print("module1: I'm in module 1 root section")
def output_hw():
print("module1: Hello world - output_hw 1")
#module2.py
import module1
print("module2: I'm in root section of module 2")
def output_hw():
print("module2: Hello world - output_hw 2")
#main_file.py
print("main_file: starting code")
# import module1
import module2
print("main_file: I'm in the root section ")
if __name__ == '__main__':
print("main_file: ******* starting __main__ section")
module2.output_hw()
# module2.output_hw()
print("main_file: Main file done!")
Output:

Now notice that module1 gets imported and executed from module2. Notice that the first line is “module1: I’m in module 1 root section” since the very first line of module2 is to import module1!
How do you make a package in your python project
To create a package it’s fairly straightforward. You simply need to move all your files into a directory and then create a file called __init__.py.
This means your directory structure looks like this:
/main_file.py
└── package1/
├── __init__.py
├── module1.py
└── module2.py
The above example, would now look like the following:
#__init__py
import package1.module1
import package1.module2
#module1.py
print("module1: I'm in module 1 root section")
def output_hw():
print("module1: Hello world - output_hw 1")
#module2.py
import package1.module1
print("module2: I'm in root section of module 2")
def output_hw():
print("module2: Hello world - output_hw 2")
#main_file.py
print("main_file: starting code")
import package
print("main_file: I'm in the root section ")
if __name__ == '__main__':
print("main_file: ******* starting __main__ section")
package1.module1.output_hw()
package1.module2.output_hw()
print("main_file: Main file done!")
So in the __init__.py file, it imports module1 & module2. The reason this is important is because so that when in main_file the package1 is imported, then it will have immediate access to module1 and module2. This is why the package1.module1 and package1.module2 works.
You cannot make the inclusion of modules automatic, and generally you shouldn’t as you may have name clashes which you can avoid if you do this manually.
Can you avoid typing the prefix of “package1” each time? Yes in fact if you use the “from”. See next section.
Only Import a part of a module
You can also import just either a class or a function of a given module if you prefer in order to limit what is accessible in your local code. However, it does still execute your whole module though. It is more a means to make your code much more readable. See the following example:
#module1.py
print("module1: I'm in module 1 root section")
def output_hw():
print("module1: Hello world - output_hw 1")
#main_file.py
print("main_file: starting code")
from module1 import output_hw
print("main_file: I'm in the root section ")
if __name__ == '__main__':
print("main_file: ******* starting __main__ section")
output_hw()
print("main_file: Main file done!")
Output

As can be seen in the above output, although just the output_hw() function is being imported, the statement “module1: Im in module1 root section” was still executed.
Note also, that you do not need to mention the module prefix in the code, you can just refer to the function as is.
So back to above, for the packages, instead of the following:
import package1.module1
you can instead use the “from” keyword but force to check local directory:
from .module1 import *
There’s a few things going on here. The '.' in front of module1 is referring to the current directory. If you wanted to check the parent directory then you can use two '.'s so the line looks like this: from ..module1 import *. The second item is that everything is being imported with the import * section.
Importing a module and applying an alias
In case you wanted to make your code easier to read, or you wanted to avoid any name clashes (see at the start of the article how module1 and module2 both had the same function name of output_hw() ), you can use the “as” keyword at the import statement to give an alternative name.
You can do the following:
#main_file.py
print("main_file: starting code")
from module1 import output_hw as module1__output_hw
print("main_file: I'm in the root section ")
if __name__ == '__main__':
print("main_file: ******* starting __main__ section")
module1__output_hw()
print("main_file: Main file done!")
This can also be done with the module or package name as well, i.e.
import module1 as mod1
Importing modules outside your project folder
Modules can by default be imported from the sub-directories up to the main script file. So the following works:
/main_file.py
└── package1/
│ ├── __init__.py
│ ├── module1.py
│ └── module2.py
└── package2/
├── __init__.py
└── pkg2_mod_a.py
Then in module1, you can import from pkg2_mod_2 with the following:
#module1.py
from package2.pkg2_mod_a import get_main_list
def output_hw():
print("module1: List from pkg2 module A:" + str( get_main_list()) )
Just need to remember in package2/__init__.py that you have to import pkg2_mod_a.py
However, what if the code was outside your main running script? Suppose if you had the following directory structure:
/
└── server_key.py
/r1/
└── main_file.py
└── package1/
├── __init__.py
└── module1.py
From any file in the /r1/ project, if you tried to import a file from server_key.py , you will get the error:
ValueError: attempted relative import beyond top-level package
To resolve this, you can in fact tell python where to look. Python keeps track of all the directories to search for modules under sys.path folder. Hence, the solution is to add an entry for the parent directory. Namely:
import sys
sys.path.append("..")
So the full code looks like the following:
#main_file.py
import sys
sys.path.append("..")
print("main_file: starting code")
import package1
print("main_file: I'm in the root section ")
if __name__ == '__main__':
print("main_file: ******* starting __main__ section")
package1.module1.output_hw()
print("main_file: Main file done!")
#module1.py
from package2.pkg2_mod_a import get_main_list
from server_key import get_server_master_key
def output_hw():
print("module1: List from pkg2 module A:" + str( get_main_list()) )
print("module1: server key :" + get_server_master_key() )
#server_key.py
def get_server_master_key():
return "AA33FF1255";
Output – The output is as follows:

How to import modules dynamically
All of the above is when you know exactly what the module name to import. However, what if you don’t know the module name until runtime?
This is where you can use the __import__ and the getattr functions to achieve this.
Firstly the getattr(). This function is used to in fact load an object dynamically where you can specify the object name in a string, or provide a default.
Secondly, the __import__() can be used to provide a module name as a string.
When you combine the two together, you first load the module with __import__, and then use getattr to load the actual function you want to call or class you want to load from the import.
See the following example:
/r1/
└── main_file.py
└── package1/
├── __init__.py
└── module1.py
With the following code:
#module1.py
def output_hw():
print("module1: take me to a funky town")
#main_file.py
if __name__ == '__main__':
print("main_file: ******* starting __main__ section")
module = __import__( 'package1.module1')
func = getattr( module, 'output_hw', None)
if func:
func()
print("main_file: Main file done!")
In the above code, we first load the module called “package1.module1” which only loads the module. Then the getattr is called on the module and then the function is passed as a string. You can also pass in a class name if you wish.
Conclusion
There are many ways to import files and to organize your projects into smaller chunks. The most difficult piece is to decide what parts of your code go where..
Get notified automatically of new articles
We are always here to help provide useful articles with usable ode snippets. Sign up to our newsletter and receive articles in your inbox automatically so you won’t miss out on the next useful tips.
How To Use Python viztracer for Code Execution Visualization
Intermediate
You run your Python script, it takes longer than expected, and you have no idea where the time is going. Adding print(time.time()) calls everywhere is slow, noisy, and still leaves you guessing which nested function call is the real culprit. cProfile gives you totals, but when a function is called 10,000 times from 50 different places, a flat table of numbers does not show you the story of how your program actually ran.
This is exactly what viztracer was built to solve. It records every function call, return, and exception in your program and saves the timeline to a JSON file that you can open as an interactive flame graph in your browser. You can zoom, filter, and inspect individual microsecond-level spans — all without adding a single line of instrumentation code to your program.
In this article we will cover how to install viztracer and trace your first script from the command line, how to use the context manager and decorator for targeted tracing, how to filter noisy built-in calls, how to add custom events and variables to the trace, and how to build a practical profiling workflow around a real script. By the end you will be able to open any Python program and see exactly where execution time is being spent, down to the function call level.
Tracing a Script with viztracer: Quick Example
The fastest way to get started is the command-line runner. Install viztracer, point it at your script, and open the result.
# install viztracer
pip install viztracer
# quick_trace.py
import time
def slow_step():
time.sleep(0.05)
def fast_step():
return sum(range(10_000))
def main():
for _ in range(3):
slow_step()
result = fast_step()
print(f"Result: {result}")
if __name__ == "__main__":
main()
Now trace it:
viztracer quick_trace.py
Result: 49995000
viztracer: Saving trace data to result.json ...
viztracer: Trace saved. File size: 42.3 KB
viztracer: Open with: vizviewer result.json
Open the trace in your browser:
vizviewer result.json
A browser tab opens at http://localhost:9001 showing a flame graph. Each horizontal bar is a function call — wider bars took longer. The three slow_step calls will be immediately visible as wide spans at the top, with time.sleep beneath them. fast_step will appear as a narrow bar, showing that sum(range(...))) is fast.
That is the core workflow: trace, view, find the bottleneck. The sections below show you how to control exactly what gets recorded and how to add your own annotations to the trace.
What Is viztracer and Why Use It?
viztracer is a low-overhead Python tracing tool that records the precise start and end time of every function call your program makes and writes that data to a JSON file following the Perfetto trace format. The viewer — either vizviewer locally or https://ui.perfetto.dev/ in the browser — renders the data as an interactive flame graph where:
- The x-axis is time — events further right happened later
- The y-axis is call depth — each row is one level of the call stack
- Bar width represents duration — wider means slower
This is fundamentally different from a flat profiler like cProfile, which only tells you aggregated totals. viztracer shows you the execution sequence — which function called which, in which order, and how long each call in context took. The table below compares the two approaches:
| Feature | cProfile | viztracer |
|---|---|---|
| Output type | Flat call-count table | Interactive flame graph timeline |
| Call sequence visible | No | Yes |
| Per-call timing | Totals only | Every individual call |
| Overhead | Low | Low (C extension, ~2-3x slowdown) |
| Filter noisy calls | Limited | Yes (max_stack_depth, ignore_frozen) |
| Custom events | No | Yes (add_instant, log_var) |
| Async/thread support | Limited | Yes |
| Output format | pstats / text | Perfetto JSON (browser-compatible) |
viztracer uses a C extension to intercept Python’s sys.setprofile at a low level, keeping overhead manageable. For most programs you can expect a 2-3x slowdown during tracing — fast enough that your program still runs in a reasonable time, slow enough that you should not leave tracing on in production.
Installation and Setup
viztracer requires Python 3.6 or later. Install it from PyPI:
# install_viztracer.sh
pip install viztracer
Verify the install:
viztracer --version
viztracer 1.0.x
On systems where viztracer is not on your PATH after installation, use python -m viztracer instead of the bare command. Both forms accept the same arguments.
Command-Line Tracing
The CLI is the easiest entry point. You do not need to modify your source file at all — viztracer wraps the script and instruments it automatically.
Basic Trace
Run any Python script through viztracer by placing it after the viztracer command, followed by any arguments your script normally receives:
# trace_cli.sh
viztracer my_script.py arg1 arg2
# Or if the command isn't on PATH:
python -m viztracer my_script.py arg1 arg2
viztracer: Saving trace data to result.json ...
viztracer: Trace saved. File size: 1.8 MB
The output file is always result.json in the current directory unless you specify otherwise with -o. For runs you want to keep, name the output explicitly:
# named_output.sh
viztracer -o traces/run_2026-07-21.json my_script.py
Limiting Depth to Reduce Noise
Large programs — especially those using libraries like NumPy or Django — generate thousands of short internal calls that clutter the trace. The --max_stack_depth flag records only calls up to a given nesting depth:
# depth_limited.sh
viztracer --max_stack_depth 10 my_script.py
A depth of 10 usually captures all of your application logic while filtering out deep library internals. If you still see too much noise, pair this with --ignore_frozen to skip modules loaded from frozen (.pyc) bytecode:
# ignore_frozen.sh
viztracer --max_stack_depth 10 --ignore_frozen my_script.py
viztracer: Saving trace data to result.json ...
viztracer: File size: 0.3 MB (was 1.8 MB without filters)
The filtered trace will be 5-10x smaller and much easier to navigate in the viewer.
Using viztracer as a Context Manager
When you only want to trace a specific section of a larger program — not the entire startup and teardown — use the VizTracer context manager. Import it, wrap the target block, and the output file is written automatically when the with block exits.
# context_manager.py
import time
from viztracer import VizTracer
def prepare_data():
"""Simulate a setup step we do NOT want to trace."""
time.sleep(0.1)
return list(range(1000))
def process(data):
return [x * x for x in data]
def save_results(results):
total = sum(results)
print(f"Total: {total}")
data = prepare_data() # not traced
with VizTracer(output_file="process_only.json"):
results = process(data) # traced
save_results(results) # not traced
Total: 332833500
viztracer: Saving trace data to process_only.json ...
Only the process(data) call and everything it calls internally appear in the trace. The prepare_data() and save_results() calls are invisible. This is the right approach when your application has a long initialization phase that would swamp the trace data you actually care about.
You can also pass configuration options directly to the constructor:
# context_manager_options.py
from viztracer import VizTracer
with VizTracer(
output_file="filtered.json",
max_stack_depth=8,
ignore_frozen=True,
log_gc=False, # exclude garbage collector events
min_duration=100, # microseconds -- skip calls shorter than 100 us
):
my_heavy_function()
The min_duration filter is particularly useful: it removes all the tiny one-microsecond function calls that are technically accurate but visually irrelevant, leaving only the spans that actually matter to your investigation.
Using the @trace_and_save Decorator
When you want to trace a single function every time it is called, without wrapping every call site in a with block, use the @trace_and_save decorator from viztracer:
# decorator_example.py
from viztracer import trace_and_save
import time
@trace_and_save(output_file="sort_trace.json")
def merge_sort(arr):
"""Classic recursive merge sort."""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
return result + left[i:] + right[j:]
import random
data = random.sample(range(10_000), 500)
sorted_data = merge_sort(data)
print(f"Sorted {len(sorted_data)} elements")
Sorted 500 elements
viztracer: Saving trace data to sort_trace.json ...
When you open this trace you will see the recursive call tree of merge_sort as a deep nested flame graph -- each recursive call spawning two children below it. This makes the O(n log n) behavior of merge sort immediately visible as a visual structure, which is a fantastic teaching tool as well as a debugging aid.
Adding Custom Events and Variables
Sometimes raw function timings are not enough -- you also want to record what values your variables held at key points in the trace. viztracer provides two methods on the tracer object: add_instant for point-in-time events and log_var for recording a variable's value.
# custom_events.py
from viztracer import VizTracer
import time
def process_batch(tracer, batch_id, items):
tracer.add_instant(
name=f"batch_start",
args={"batch_id": batch_id, "size": len(items)},
)
result = []
for item in items:
processed = item ** 2
result.append(processed)
tracer.log_var("batch_result_sum", sum(result))
return result
with VizTracer(output_file="custom_events.json") as tracer:
all_results = []
for i, batch in enumerate([[1,2,3], [4,5,6], [7,8,9]]):
batch_output = process_batch(tracer, i, batch)
all_results.extend(batch_output)
print(f"Final sum: {sum(all_results)}")
Final sum: 285
viztracer: Saving trace data to custom_events.json ...
In the flame graph viewer, add_instant events appear as vertical markers on the timeline -- thin lines you can click to see the args dictionary. log_var results appear as a separate row below the call stack showing how the variable's value changed over time. Both are invaluable when you need to correlate a performance spike with a particular input state.
Tracing Async and Multi-threaded Code
viztracer handles asyncio coroutines and threads without any special configuration. Async tasks appear as interleaved spans in the timeline -- you can see exactly when a coroutine yielded to the event loop and when it was resumed. Threads each get their own row in the flame graph, making it easy to spot thread synchronization delays.
# async_trace.py
import asyncio
import time
from viztracer import VizTracer
async def fetch_page(url_id):
# Simulate variable-latency I/O
await asyncio.sleep(0.02 * (url_id % 3 + 1))
return f"page_{url_id}_content"
async def main():
tasks = [fetch_page(i) for i in range(6)]
results = await asyncio.gather(*tasks)
print(f"Fetched {len(results)} pages")
with VizTracer(output_file="async_trace.json"):
asyncio.run(main())
Fetched 6 pages
viztracer: Saving trace data to async_trace.json ...
In the viewer the six coroutines run concurrently on the same thread row, with gaps visible where each was awaiting I/O. You can click any span to see its full name, start time, and duration -- which lets you confirm that the asyncio.gather call saved time by running the fetches in parallel rather than sequentially.
Real-Life Example: Profiling a Data Pipeline
Here is a realistic scenario: a CSV processing pipeline that reads rows, cleans them, and writes a summary. We want to know which step is the bottleneck before deciding whether to optimize or parallelize.
# data_pipeline.py
import csv
import io
import time
from viztracer import VizTracer
# Simulate an in-memory CSV with 50,000 rows
def generate_csv(rows=50_000):
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["id", "value", "category"])
for i in range(rows):
writer.writerow([i, i * 1.5, f"cat_{i % 10}"])
output.seek(0)
return output
def read_rows(csv_file):
"""Step 1: Read all rows into memory."""
reader = csv.DictReader(csv_file)
return list(reader)
def clean_rows(rows):
"""Step 2: Parse and validate each row."""
cleaned = []
for row in rows:
try:
cleaned.append({
"id": int(row["id"]),
"value": float(row["value"]),
"category": row["category"].strip(),
})
except (ValueError, KeyError):
pass # skip malformed rows
return cleaned
def summarize(rows):
"""Step 3: Compute per-category totals."""
totals = {}
for row in rows:
cat = row["category"]
totals[cat] = totals.get(cat, 0) + row["value"]
return totals
def run_pipeline():
csv_file = generate_csv(50_000)
rows = read_rows(csv_file)
cleaned = clean_rows(rows)
summary = summarize(cleaned)
return summary
with VizTracer(
output_file="pipeline_trace.json",
max_stack_depth=10,
ignore_frozen=True,
):
result = run_pipeline()
# Print a sample of the summary
for category, total in sorted(result.items())[:3]:
print(f"{category}: {total:.1f}")
print(f"Categories processed: {len(result)}")
cat_0: 37496250.0
cat_1: 37497750.0
cat_2: 37499250.0
Categories processed: 10
viztracer: Saving trace data to pipeline_trace.json ...
Open pipeline_trace.json and you will immediately see the relative widths of read_rows, clean_rows, and summarize. In practice clean_rows is the widest bar because it calls int() and float() 50,000 times in a loop. That is the function to optimize first -- perhaps by switching to NumPy for the type conversions or by filtering at read time with a streaming approach. viztracer showed you which step to target in a single trace run, saving you the time of guessing or profiling each function separately.
Frequently Asked Questions
How much does viztracer slow down my program?
viztracer uses a C extension for instrumentation, which keeps overhead low. For most programs you can expect a 2-3x slowdown compared to running without tracing. This means a 1-second script takes 2-3 seconds under viztracer. If your program is heavily recursive or calls millions of tiny functions, the overhead can be higher. You can reduce it significantly with --max_stack_depth to limit how many call levels are recorded, and min_duration to skip very short calls. For performance-critical measurement, do a baseline run without viztracer first and compare ratios rather than absolute numbers.
My result.json is several hundred megabytes. How do I handle that?
Very long-running programs or programs that make millions of calls generate large trace files. Use two strategies: first, limit scope with the context manager or decorator so you only trace the section you care about. Second, use the tracer_entries argument to set a circular buffer size -- VizTracer(tracer_entries=1_000_000) keeps only the last 1 million events and discards older ones. The vizviewer at ui.perfetto.dev handles files up to a few hundred MB without issue. For files larger than that, filter at the command line with viztracer --max_stack_depth 5 to produce a smaller trace from the start.
Does viztracer work with multiprocessing?
Yes. Pass pid_suffix=True to the VizTracer constructor and each process will write its own trace file with the PID appended to the filename (e.g., result_12345.json, result_12346.json). You can then merge multiple JSON traces into a single view using viztracer --combine result_12345.json result_12346.json -o combined.json. The combined view shows all processes on separate rows, which is useful for spotting inter-process synchronization delays.
When should I use viztracer vs cProfile?
Use cProfile when you need a quick aggregate answer: "which function is called the most?" or "where does the total CPU time go?" It is built into Python and adds minimal overhead. Use viztracer when you need to understand the sequence of execution: "why is this function slow only on the third call?" or "which code path is hitting this bottleneck?" viztracer also handles async and threading better than cProfile. A good workflow is to use cProfile for an initial survey and then viztracer to drill into the hot spot.
Can I use viztracer with Django or Flask?
Yes, but with care. You typically do not want to trace an entire web server process. Instead, wrap specific view functions or middleware calls using the VizTracer context manager inside the function body. For Django management commands, you can trace the entire command with the CLI runner: viztracer manage.py my_command --max_stack_depth 15. Keep ignore_frozen=True to exclude Django's internal machinery and focus on your application code. Because web servers are long-running, always use tracer_entries to limit buffer size and avoid unbounded memory growth.
Can I export the trace to formats other than JSON?
viztracer outputs Perfetto-compatible JSON by default, which is the most feature-rich format. You can view it with vizviewer result.json (local, no internet needed) or upload it to https://ui.perfetto.dev/ for a more polished interface. For CI pipelines or automated comparison, the JSON is easy to parse programmatically -- the traceEvents key holds a list of span objects with name, ts (timestamp in microseconds), and dur (duration) fields. There is no built-in HTML or PDF export, but you can write a simple script to pull the top-N slowest spans from the JSON and report them in any format you need.
Conclusion
viztracer makes the invisible visible. Instead of staring at aggregate numbers from cProfile and guessing which call path is slow, you get an interactive timeline that shows exactly what your program did, when it did it, and how long each step took. We covered tracing from the command line with viztracer my_script.py, focusing traces with the VizTracer context manager and @trace_and_save decorator, reducing noise with --max_stack_depth and --ignore_frozen, annotating traces with add_instant and log_var, and tracing async and multi-threaded programs without any special configuration.
The next step is to run viztracer against a real bottleneck in your own codebase. Wrap the slow function in a with VizTracer() block, open the flame graph, and look for the widest bar that is not a built-in. That is your target. From there you can decide whether to optimize the algorithm, cache the result, or move the work to a background thread -- and you can make that decision based on evidence rather than intuition.
For the full list of CLI flags, configuration options, and advanced features like remote profiling and snapshot mode, see the official viztracer documentation.
Related Articles
Related Articles
- How To Split And Organise Your Source Code Into Multiple Files in Python 3
- How To Use Argv and Argc Command Line Parameters in Python
- How To Use the Logging Module in Python 3
Further Reading: For more details, see the Python import system documentation.
Frequently Asked Questions
What is the difference between absolute and relative imports in Python?
Absolute imports use the full package path from the project root (e.g., from mypackage.module import func). Relative imports use dots to reference the current package (e.g., from .module import func). Absolute imports are generally preferred for clarity.
What does __init__.py do in a Python package?
The __init__.py file marks a directory as a Python package, allowing its modules to be imported. It can be empty or contain initialization code, define __all__ for controlling wildcard imports, or re-export symbols for a cleaner public API.
How do I fix ‘ModuleNotFoundError’ in Python?
Check that the module is installed (pip install), verify your PYTHONPATH includes the right directories, ensure __init__.py files exist in package directories, and confirm you are using the correct Python environment. Running from the project root often resolves path issues.
What is the best project structure for a Python application?
A common structure includes a top-level project directory containing a src/ folder with your package, a tests/ folder, setup.py or pyproject.toml, and a requirements.txt. This keeps source code, tests, and configuration clearly separated.
Should I use relative or absolute imports?
PEP 8 recommends absolute imports for most cases because they are more readable and less error-prone. Use relative imports only within a package when the internal structure is unlikely to change and the import path would be excessively long with absolute imports.
Continue Learning Python
Tutorials you might also find useful: