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.
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 Build CLI Apps with Python Click
Intermediate
Every serious Python developer eventually needs to build a command-line interface. Whether it is a deployment tool, a data processing script, or a developer utility, a well-designed CLI makes the difference between a tool your team actually uses and one that sits forgotten. Python’s standard argparse module works, but it is verbose — you write 20 lines of setup code before you handle your first argument. Click is the modern alternative: decorator-based, expressive, and composable, it cuts that boilerplate in half and adds features argparse simply does not have.
Click was created by the team behind Flask and follows the same philosophy: explicit is better than implicit, but explicit does not have to be painful. You decorate a Python function with @click.command() and @click.option(), and Click handles argument parsing, help text, type conversion, validation, and error messages automatically. Install it with pip install click.
This article covers everything you need to build production-quality CLI tools with Click: basic commands and options, arguments, type validation, prompts, multi-command groups (subcommands), progress bars, and output formatting. By the end, we will build a complete file management CLI that demonstrates all these features working together.
Click Quick Example
Here is a complete Click CLI that greets a user, with an optional count parameter:
# quick_click.py
import click
@click.command()
@click.option('--name', default='World', help='Who to greet.')
@click.option('--count', default=1, type=int, help='Number of greetings.')
@click.option('--loud', is_flag=True, help='Use uppercase.')
def greet(name, count, loud):
"""A friendly greeting command."""
for _ in range(count):
message = f"Hello, {name}!"
if loud:
message = message.upper()
click.echo(message)
if __name__ == '__main__':
greet()
Run it from the terminal:
$ python quick_click.py --name Alice --count 3
Hello, Alice!
Hello, Alice!
Hello, Alice!
$ python quick_click.py --name Bob --loud
HELLO, BOB!
$ python quick_click.py --help
Usage: quick_click.py [OPTIONS]
A friendly greeting command.
Options:
--name TEXT Who to greet.
--count INTEGER Number of greetings.
--loud Use uppercase.
--help Show this message and exit.
Click generated a complete help page automatically from the function’s docstring and decorator metadata. The --help flag, type validation, and default values all come for free.
Options vs Arguments
Click distinguishes between two kinds of inputs: options (named flags like --name Alice) and arguments (positional inputs like a filename). Options are optional by default; arguments are required by default.
| Feature | Option (@click.option) | Argument (@click.argument) |
|---|---|---|
| Syntax | --flag value | Positional: cmd value |
| Required | Optional by default | Required by default |
| Help text | Shown in --help | Shown in usage line |
| Best for | Configuration, flags | Primary inputs (files, names) |
# options_arguments.py
import click
@click.command()
@click.argument('filename') # Required positional arg
@click.option('--output', '-o', default='-', # -o is a short alias
help='Output file (default: stdout)')
@click.option('--lines', '-n', default=10,
type=int, help='Number of lines to show.')
@click.option('--verbose', '-v', is_flag=True,
help='Show extra information.')
def head(filename, output, lines, verbose):
"""Show the first N lines of FILENAME."""
if verbose:
click.echo(f"Reading {filename}, showing {lines} lines")
try:
with open(filename) as f:
for i, line in enumerate(f):
if i >= lines:
break
click.echo(line, nl=False)
except FileNotFoundError:
click.echo(f"Error: {filename} not found", err=True)
raise SystemExit(1)
if __name__ == '__main__':
head()
Run it as python options_arguments.py myfile.txt --lines 5 --verbose. The -o short alias for --output is defined right in the option decorator. Click handles both -o file.txt and --output file.txt automatically.
Types and Validation
Click converts option and argument values to the specified Python type and shows a helpful error if the conversion fails. Beyond basic types, Click has specialized types like click.Path for file paths and click.Choice for enumerated values.
# types_demo.py
import click
@click.command()
@click.argument('input_file', type=click.Path(exists=True, readable=True))
@click.option('--format', 'output_format',
type=click.Choice(['json', 'csv', 'text'], case_sensitive=False),
default='text', help='Output format.')
@click.option('--max-size', type=click.IntRange(1, 1000),
default=100, help='Max size (1-1000).')
@click.option('--scale', type=float, help='Scaling factor.')
def process(input_file, output_format, max_size, scale):
"""Process INPUT_FILE with validation."""
click.echo(f"Processing: {input_file}")
click.echo(f"Format: {output_format}")
click.echo(f"Max size: {max_size}")
if scale:
click.echo(f"Scale: {scale}")
if __name__ == '__main__':
process()
When you pass an invalid value, Click provides a clear error message:
$ python types_demo.py myfile.txt --format xml
Error: Invalid value for '--format': 'xml' is not one of 'json', 'csv', 'text'.
$ python types_demo.py nonexistent.txt
Error: Invalid value for 'INPUT_FILE': Path 'nonexistent.txt' does not exist.
click.Path(exists=True) validates the file exists before your function even runs. click.IntRange(1, 1000) ensures the integer is within bounds. These validations happen automatically and produce user-friendly error messages — no manual error handling needed.
Interactive Prompts and Confirmation
For destructive operations, you often want to confirm with the user. Click provides @click.confirmation_option(), @click.password_option(), and click.prompt() for interactive input collection.
# prompts_demo.py
import click
@click.command()
@click.option('--username', prompt='Username',
help='Your username.')
@click.option('--password', prompt=True,
hide_input=True, confirmation_prompt=True,
help='Your password.')
@click.option('--database', prompt='Database name',
default='mydb', show_default=True)
def setup_connection(username, password, database):
"""Set up a database connection."""
click.echo(f"Connecting to {database} as {username}...")
click.echo(f"Password length: {len(password)} chars")
# In a real app, you'd use these to create a connection
click.echo("Connection configured successfully!")
@click.command()
@click.argument('filename')
@click.confirmation_option(prompt='Are you sure you want to delete this file?')
def delete_file(filename):
"""Permanently delete FILENAME."""
import os
try:
os.remove(filename)
click.echo(f"Deleted: {filename}", err=False)
except FileNotFoundError:
click.echo(f"File not found: {filename}", err=True)
if __name__ == '__main__':
setup_connection()
Run python prompts_demo.py and Click interactively prompts for each required value. The password is hidden during input (no echo to terminal) and asks for confirmation. The @click.confirmation_option adds a yes/no prompt before any destructive action — and automatically processes -y or --yes flags to skip the prompt in automated scripts.
Multi-Command Groups (Subcommands)
Real CLI tools like git and docker use subcommands: git commit, git push, docker build, docker run. Click’s @click.group() decorator creates this structure cleanly. Each subcommand is just another decorated function.
# groups_demo.py
import click
@click.group()
@click.option('--debug/--no-debug', default=False,
help='Enable debug output.')
@click.pass_context
def cli(ctx, debug):
"""Project management tool."""
ctx.ensure_object(dict)
ctx.obj['DEBUG'] = debug
@cli.command()
@click.argument('name')
@click.option('--template', default='basic',
type=click.Choice(['basic', 'flask', 'fastapi']),
help='Project template.')
@click.pass_context
def create(ctx, name, template):
"""Create a new project."""
if ctx.obj['DEBUG']:
click.echo(f"[DEBUG] Creating {name} with template {template}")
click.echo(f"Creating project '{name}'...")
click.echo(f"Template: {template}")
click.echo(f"Done! Run: cd {name} && python main.py")
@cli.command()
@click.argument('name')
@click.pass_context
def delete(ctx, name):
"""Delete a project."""
if ctx.obj['DEBUG']:
click.echo(f"[DEBUG] Deleting {name}")
click.confirm(f"Delete project '{name}'? This cannot be undone.", abort=True)
click.echo(f"Project '{name}' deleted.")
@cli.command()
@click.pass_context
def list_projects(ctx):
"""List all projects."""
click.echo("Projects:")
for project in ['api-service', 'data-pipeline', 'dashboard']:
click.echo(f" - {project}")
# Register the list command with a different name
cli.add_command(list_projects, name='list')
if __name__ == '__main__':
cli()
Run it as:
$ python groups_demo.py --help
Usage: groups_demo.py [OPTIONS] COMMAND [ARGS]...
Project management tool.
Options:
--debug / --no-debug Enable debug output.
--help Show this message and exit.
Commands:
create Create a new project.
delete Delete a project.
list List all projects.
$ python groups_demo.py create myapp --template flask
Creating project 'myapp'...
Template: flask
Done! Run: cd myapp && python main.py
$ python groups_demo.py --debug create myapp
[DEBUG] Creating myapp with template basic
Creating project 'myapp'...
The ctx.pass_context pattern passes a shared context object through all subcommands. The --debug flag is defined on the group level and passed down through context — this is the Click pattern for global flags that affect all subcommands.
Real-Life Example: A File Processing CLI
Here is a complete, practical CLI tool for processing text files — counting words, searching for patterns, and converting case — with progress bars for large files.
# filetools.py
import click
import re
from pathlib import Path
@click.group()
def cli():
"""File processing toolkit."""
@cli.command()
@click.argument('files', nargs=-1, type=click.Path(exists=True), required=True)
@click.option('--words/--no-words', default=True, help='Count words.')
@click.option('--lines/--no-lines', default=True, help='Count lines.')
@click.option('--chars/--no-chars', default=False, help='Count characters.')
def count(files, words, lines, chars):
"""Count words/lines/chars in FILES."""
total_w, total_l, total_c = 0, 0, 0
for filepath in files:
content = Path(filepath).read_text()
w = len(content.split())
l = content.count('\n')
c = len(content)
total_w += w; total_l += l; total_c += c
parts = []
if lines: parts.append(f"{l:>8} lines")
if words: parts.append(f"{w:>8} words")
if chars: parts.append(f"{c:>8} chars")
click.echo(f"{' '.join(parts)} {filepath}")
if len(files) > 1:
click.echo(f"{'':->40}")
click.echo(f"{total_l:>8} lines {total_w:>8} words total")
@cli.command()
@click.argument('pattern')
@click.argument('files', nargs=-1, type=click.Path(exists=True), required=True)
@click.option('--ignore-case', '-i', is_flag=True, help='Case-insensitive.')
@click.option('--count-only', '-c', is_flag=True, help='Print match count only.')
def search(pattern, files, ignore_case, count_only):
"""Search for PATTERN in FILES."""
flags = re.IGNORECASE if ignore_case else 0
for filepath in files:
content = Path(filepath).read_text()
matches = [(i+1, line) for i, line in enumerate(content.splitlines())
if re.search(pattern, line, flags)]
if count_only:
click.echo(f"{len(matches):>5} {filepath}")
else:
for lineno, line in matches:
click.secho(f"{filepath}:{lineno}: ", nl=False, fg='cyan')
# Highlight the match in yellow
highlighted = re.sub(pattern,
lambda m: click.style(m.group(), fg='yellow', bold=True),
line, flags=flags)
click.echo(highlighted)
if __name__ == '__main__':
cli()
Run as:
$ python filetools.py count README.md
45 lines 312 words README.md
$ python filetools.py search "import" *.py --ignore-case
filetools.py:1: import click
filetools.py:2: import re
filetools.py:3: from pathlib import Path
The nargs=-1 pattern on FILES accepts any number of file arguments, like the Unix convention. click.secho() combines echo with styled output (colors). The --ignore-case short alias -i matches grep’s convention, making the tool feel natural to Unix users.
Frequently Asked Questions
When should I use Click instead of argparse?
Use Click for new CLI tools — it is less verbose and more composable. argparse is already in the standard library and requires no installation, so it is better for simple scripts that need zero dependencies. Click shines for multi-command CLIs with many options, complex validation, interactive prompts, and colored output. If you are building something beyond a simple script, Click’s developer experience wins decisively.
How does Click compare to Typer?
Typer is built on top of Click and generates Click CLI definitions from Python function type hints. If you use type annotations throughout your code, Typer reduces Click boilerplate further — you get options and arguments from type hints with no decorators. The trade-off: Typer adds a dependency and is less flexible than Click for complex CLI patterns. Click is more explicit; Typer is more magic. Both are excellent choices.
How do I test Click commands?
Click provides a CliRunner for testing. Use from click.testing import CliRunner; runner = CliRunner(); result = runner.invoke(my_command, ['--option', 'value']). The result object has exit_code, output, and exception attributes. This lets you test CLI behavior in pytest without spawning a subprocess, and it works with input prompts by passing input='yes\n' to invoke().
Can Click read options from environment variables?
Yes. Set auto_envvar_prefix='MYAPP' on the group, and Click automatically reads MYAPP_OPTION_NAME from the environment for any option not provided on the command line. You can also set it per-option: @click.option('--api-key', envvar='API_KEY'). This is the standard pattern for 12-factor applications where configuration comes from the environment.
How do I package a Click app as a proper CLI command?
Add an entry_points section to your pyproject.toml: [project.scripts] mytool = "mypackage.cli:main". After pip install -e ., running mytool in the terminal invokes your Click function directly. This is the standard way to distribute CLI tools on PyPI — users install your package and get the command available system-wide.
Conclusion
We covered the full Click toolkit: defining commands with @click.command(), options with @click.option(), arguments with @click.argument(), type validation with click.Path and click.Choice, interactive prompts, multi-command groups with shared context using @click.pass_context, and colored output with click.secho(). The file processing CLI showed how to compose these features into a tool that feels like a native Unix command.
From here, explore Click’s progress bar support (click.progressbar()), file path handling with lazy file opening, and the CliRunner for testing. Click’s plugin system also allows distributing CLI extensions as separate packages — the same pattern used by Flask extensions.
Official documentation: click.palletsprojects.com
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.