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 Enums and When You Should
Intermediate
You’ve seen it in every codebase: a string constant like "pending" scattered across thirty files, a magic integer like 2 that means “admin role” but is never defined anywhere, a status field that accepts anything and silently breaks when someone types "Pending" with a capital P. These are the symptoms of code that uses raw literals instead of named constants — and Python’s enum module is the standard fix. Enums are not just constants with names. They are types with identity, comparison semantics, iteration support, and first-class integration with type checkers and Python’s match statement.
The enum module ships in the Python standard library — no installation required. It has been available since Python 3.4, and the variants you need most (Enum, IntEnum, StrEnum, Flag) are all in one import. Python 3.11 added StrEnum and improved IntEnum behavior, so the examples in this article require Python 3.11 or later for the StrEnum sections. Everything else works from Python 3.4 onward.
This article covers everything you need to use enums confidently. We’ll start with a quick working example, then walk through the core enum types (Enum, IntEnum, StrEnum, Flag), iteration and comparison, using enums in match statements, and when NOT to use them. There is a real-life project at the end — a task management system — that ties all of it together.
Python Enums: Quick Example
This minimal example shows the core pattern. Define constants once, reference them by name everywhere, and let Python handle comparison and validation.
# enums_quick.py
from enum import Enum, auto
class OrderStatus(Enum):
PENDING = auto()
PROCESSING = auto()
SHIPPED = auto()
DELIVERED = auto()
CANCELLED = auto()
def describe_order(status: OrderStatus) -> str:
if status == OrderStatus.PENDING:
return "Your order is waiting to be processed."
elif status == OrderStatus.SHIPPED:
return "Your order is on the way!"
elif status == OrderStatus.DELIVERED:
return "Your order has arrived."
elif status == OrderStatus.CANCELLED:
return "Your order was cancelled."
else:
return f"Order is {status.name.lower()}."
current = OrderStatus.SHIPPED
print(describe_order(current))
print(f"Status name: {current.name}")
print(f"Status value: {current.value}")
print(f"Is shipped: {current == OrderStatus.SHIPPED}")
Output:
Your order is on the way!
Status name: SHIPPED
Status value: 3
Is shipped: True
auto() assigns integer values automatically — you never have to pick or remember them. Each enum member has a .name (the Python attribute name) and a .value (the assigned value). Comparison with == checks identity, not just value — OrderStatus.SHIPPED == 3 is False unless you use IntEnum. That strictness is usually what you want, because it prevents code that accidentally mixes raw integers with enum values from silently doing the wrong thing.
What Are Enums and When Should You Use Them?
An enum (short for enumeration) is a set of named constants that form a logical group. Python’s Enum class turns these constants into a proper type: members are unique, iterable, comparable, and serializable. You can think of an enum as a named lookup table where each entry is an immutable, singleton object.
The right time to reach for an enum is whenever you have a fixed set of related values that represent distinct states, categories, or options. The wrong time is when the set of values is dynamic (loaded from a database at runtime), unbounded (any string is valid), or purely numeric with no semantic meaning (a count, an ID, a score).
| Situation | Use enum? | Why |
|---|---|---|
| Order status: pending, shipped, delivered | Yes | Fixed set, semantic meaning, type-checked |
| HTTP methods: GET, POST, PUT, DELETE | Yes | Fixed set, comparison matters, readable in logs |
| User role loaded from DB at runtime | No | Dynamic set — use a string or int validated at boundaries |
| Permission flags that combine (read | write) | Yes (Flag) | Bitwise combination is exactly what Flag is for |
| Constant pi or gravitational constant | No | Single value — use a module-level constant |
| Days of the week | Yes | Fixed, ordered, iterable — classic enum use case |
The practical test: if you find yourself writing if status == "pending" in more than one place, you should probably be writing if status == OrderStatus.PENDING instead. Enums make typos into AttributeErrors caught at load time rather than silent bugs caught at 3am.
Basic Enum: Named Constants With Identity
The base Enum class is the most common starting point. Values can be anything — integers, strings, tuples — but the members are compared by identity, not by value. This means two enums with the same value but different names are different members (unless they are aliases, which we cover below).
# basic_enum.py
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
def opposite(self) -> "Direction":
opposites = {
Direction.NORTH: Direction.SOUTH,
Direction.SOUTH: Direction.NORTH,
Direction.EAST: Direction.WEST,
Direction.WEST: Direction.EAST,
}
return opposites[self]
# Access members by name or value
print(Direction.NORTH) # Direction.NORTH
print(Direction.NORTH.name) # NORTH
print(Direction.NORTH.value) # 1
# Look up a member by value
d = Direction(2)
print(d) # Direction.SOUTH
# Look up by name
d2 = Direction["EAST"]
print(d2) # Direction.EAST
# Enum methods work naturally
current = Direction.NORTH
print(f"Heading {current.name}, opposite is {current.opposite().name}")
# Iteration
print("All directions:", [d.name for d in Direction])
# Membership test
print(Direction.WEST in Direction) # True
Output:
Direction.NORTH
NORTH
1
Direction.SOUTH
Direction.EAST
Heading NORTH, opposite is SOUTH
All directions: ['NORTH', 'SOUTH', 'EAST', 'WEST']
True
A few things to notice. You can add methods to an enum class — opposite() is a real method that returns another enum member. The Direction(2) lookup-by-value is useful when deserializing data from an API or database. The Direction["EAST"] lookup-by-name is useful when parsing config strings. Both raise ValueError or KeyError on invalid input, giving you early validation rather than silent failures downstream.
IntEnum and StrEnum: When Values Matter
The base Enum does not compare equal to its underlying value. This is deliberate — it prevents accidentally passing an integer where an enum is expected. But sometimes you genuinely need the enum value to behave like its underlying type: when writing to a database that stores integers, or when generating JSON that needs plain strings. That is where IntEnum and StrEnum come in.
# intenum_strenum.py
from enum import IntEnum, StrEnum # StrEnum requires Python 3.11+
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
CRITICAL = 4
class Color(StrEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
# IntEnum compares with integers directly
task_priority = Priority.HIGH
print(task_priority > 2) # True (behaves like int 3)
print(task_priority == 3) # True
print(sorted([Priority.CRITICAL, Priority.LOW, Priority.MEDIUM])) # sorted!
# IntEnum values work in numeric contexts
tasks_today = {Priority.LOW: 5, Priority.HIGH: 2}
total_weighted = sum(p * count for p, count in tasks_today.items())
print(f"Weighted load: {total_weighted}") # 1*5 + 3*2 = 11
# StrEnum compares with strings directly
html_color = Color.RED
print(html_color == "red") # True
print(f"CSS: color: {html_color};") # Works directly in f-strings
print(isinstance(html_color, str)) # True
# Use StrEnum in dict lookups without converting
config = {"red": "#FF0000", "green": "#00FF00", "blue": "#0000FF"}
print(config[Color.BLUE]) # #0000FF -- lookup works directly
Output:
True
True
[<Priority.LOW: 1>, <Priority.MEDIUM: 2>, <Priority.HIGH: 3>]
Weighted load: 11
True
CSS: color: red;
True
#0000FF
The tradeoff with IntEnum and StrEnum is that you give up some type safety — code that passes raw integers or strings will no longer be caught by a type checker. Use them when interoperability with external systems (databases, APIs, config files) is more important than strict type boundaries. For internal domain logic, prefer the base Enum.
Flag Enums: Combining Options
The Flag class is for situations where values can be combined — file permissions, feature flags, user capabilities. Each member is a power of two, and you combine them with the | operator. Checking membership uses the in operator or &.
# flag_enum.py
from enum import Flag, auto
class Permission(Flag):
READ = auto() # 1
WRITE = auto() # 2
EXECUTE = auto() # 4
DELETE = auto() # 8
# Convenience aliases
READ_WRITE = READ | WRITE
ADMIN = READ | WRITE | EXECUTE | DELETE
def check_access(user_perms: Permission, required: Permission) -> bool:
return required in user_perms
# Combine permissions with |
guest_perms = Permission.READ
editor_perms = Permission.READ | Permission.WRITE
admin_perms = Permission.ADMIN
print(f"Guest: {guest_perms}")
print(f"Editor: {editor_perms}")
print(f"Admin: {admin_perms}")
# Check individual permissions
print(check_access(editor_perms, Permission.WRITE)) # True
print(check_access(guest_perms, Permission.WRITE)) # False
print(check_access(admin_perms, Permission.DELETE)) # True
# Iterate over combined flags to see components
print("Admin permissions:")
for perm in admin_perms:
print(f" - {perm.name}")
Output:
Guest: Permission.READ
Editor: Permission.READ|WRITE
Admin: Permission.READ|WRITE|EXECUTE|DELETE
True
False
True
Admin permissions:
- READ
- WRITE
- EXECUTE
- DELETE
Flag is the cleanest way to represent additive permission sets in Python. The in operator checks whether a flag is set in a combination, and iterating over a combined value yields each individual flag that is included. This replaces the old pattern of bitmasking integers manually, and makes the code both readable and type-safe.
Enums in match Statements
Python 3.10’s match statement works naturally with enums and is a cleaner alternative to chains of if/elif when you need to handle multiple enum cases. The exhaustive pattern matching also helps type checkers warn you when you forget a case.
# enums_match.py
from enum import Enum, auto
class TrafficLight(Enum):
RED = auto()
YELLOW = auto()
GREEN = auto()
def get_action(light: TrafficLight) -> str:
match light:
case TrafficLight.RED:
return "Stop completely."
case TrafficLight.YELLOW:
return "Prepare to stop."
case TrafficLight.GREEN:
return "Proceed when safe."
case _:
return "Unknown signal -- stop and assess."
# Cycle through all states
for state in TrafficLight:
print(f"{state.name}: {get_action(state)}")
Output:
RED: Stop completely.
YELLOW: Prepare to stop.
GREEN: Proceed when safe.
The match statement with enums is especially readable because each case is self-documenting — case TrafficLight.RED is unambiguous in a way that case 1 never is. Type checkers like mypy can detect missing cases when combined with _ fallthrough, giving you a compile-time reminder to handle new enum members you add later.
Real-Life Example: Task Management System
This project uses enums throughout a task management system: task status, priority, and user permissions all use the appropriate enum type. The system routes tasks, checks permissions, and generates a report.
# task_manager.py
from enum import Enum, Flag, StrEnum, auto
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
class TaskStatus(Enum):
TODO = auto()
IN_PROGRESS = auto()
BLOCKED = auto()
REVIEW = auto()
DONE = auto()
class Priority(StrEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class TeamPermission(Flag):
VIEW = auto()
COMMENT = auto()
EDIT = auto()
CLOSE = auto()
MANAGE = VIEW | COMMENT | EDIT | CLOSE
@dataclass
class Task:
title: str
status: TaskStatus = TaskStatus.TODO
priority: Priority = Priority.MEDIUM
assignee: Optional[str] = None
created_at: datetime = field(default_factory=datetime.now)
def transition(self, new_status: TaskStatus) -> None:
valid_transitions = {
TaskStatus.TODO: {TaskStatus.IN_PROGRESS},
TaskStatus.IN_PROGRESS: {TaskStatus.BLOCKED, TaskStatus.REVIEW, TaskStatus.DONE},
TaskStatus.BLOCKED: {TaskStatus.IN_PROGRESS},
TaskStatus.REVIEW: {TaskStatus.IN_PROGRESS, TaskStatus.DONE},
TaskStatus.DONE: set(),
}
if new_status not in valid_transitions[self.status]:
raise ValueError(
f"Cannot transition {self.status.name} -> {new_status.name}. "
f"Valid: {[s.name for s in valid_transitions[self.status]]}"
)
self.status = new_status
def can_close_task(perms: TeamPermission) -> bool:
return TeamPermission.CLOSE in perms
def print_report(tasks: list[Task]) -> None:
by_status: dict[TaskStatus, list[Task]] = {s: [] for s in TaskStatus}
for task in tasks:
by_status[task.status].append(task)
print("\n=== Task Board Report ===")
for status, group in by_status.items():
if group:
print(f"\n[{status.name}]")
for task in group:
assignee = task.assignee or "unassigned"
print(f" [{task.priority.upper()}] {task.title} ({assignee})")
# --- Demo ---
tasks = [
Task("Write API docs", priority=Priority.HIGH, assignee="alice"),
Task("Fix login bug", priority=Priority.CRITICAL, assignee="bob"),
Task("Update dependencies", priority=Priority.LOW),
Task("Code review PR #42", priority=Priority.MEDIUM, assignee="carol"),
]
# Transition tasks through statuses
tasks[0].transition(TaskStatus.IN_PROGRESS)
tasks[1].transition(TaskStatus.IN_PROGRESS)
tasks[1].transition(TaskStatus.REVIEW)
tasks[2].transition(TaskStatus.IN_PROGRESS)
tasks[2].transition(TaskStatus.DONE)
# Permission check
dev_perms = TeamPermission.VIEW | TeamPermission.COMMENT | TeamPermission.EDIT
lead_perms = TeamPermission.MANAGE
print(f"Dev can close tasks: {can_close_task(dev_perms)}")
print(f"Lead can close tasks: {can_close_task(lead_perms)}")
# Report
print_report(tasks)
Output:
Dev can close tasks: False
Lead can close tasks: True
=== Task Board Report ===
[IN_PROGRESS]
[HIGH] Write API docs (alice)
[LOW] Update dependencies (unassigned)
[REVIEW]
[CRITICAL] Fix login bug (bob)
[DONE]
[LOW] Update dependencies (unassigned)
[TODO]
[MEDIUM] Code review PR #42 (carol)
This system uses three different enum types for three different purposes: Enum for status (identity matters, no comparison to raw values needed), StrEnum for priority (values are passed directly to display strings and API responses), and Flag for permissions (combinations are the whole point). The transition() method enforces valid state changes using the enum itself as the key in a transition table — a pattern that makes invalid state transitions into immediate ValueErrors rather than silent data corruption. You can extend this by adding a @classmethod from_string to each enum for loading from database rows or API payloads.
Frequently Asked Questions
How do I serialize enums to JSON?
The standard json module does not know how to serialize enums by default — you will get a TypeError. The cleanest fix is a custom encoder: class EnumEncoder(json.JSONEncoder): def default(self, obj): return obj.value if isinstance(obj, Enum) else super().default(obj). Then pass cls=EnumEncoder to json.dumps(). Alternatively, if you are using StrEnum or IntEnum, the values serialize directly since they are plain strings or ints. For loading from JSON, use MyEnum(raw_value) to reconstruct the enum member at the deserialization boundary.
What values does auto() assign?
auto() assigns integers starting from 1 by default, incrementing by 1 for each member. You can override this behavior by defining _generate_next_value_(name, start, count, last_values) in your enum class. For example, returning name.lower() from that method makes auto() produce lowercase string values matching the member names — a shortcut that StrEnum formalized. The value of auto() is that you never have to manually number your enum members or worry about gaps when you reorder or add members.
What are enum aliases and how do I prevent them?
If two enum members have the same value, the second is an alias for the first — accessing it returns the first member. This is intentional for cases like TUESDAY = TUES = 2. To prevent accidental aliases (and get an error if you accidentally duplicate values), use the @unique decorator from the enum module: @enum.unique above the class definition raises a ValueError at class creation time if any duplicate values exist. This is a good default for most enums where each value should be distinct.
When should I use Enum vs Literal?
typing.Literal["pending", "shipped", "done"] is a type annotation for functions that accept a fixed set of string values. It is pure typing with no runtime behavior — you can pass any string and Python will not complain at runtime, only the type checker will. Enum is a runtime construct: invalid values are caught when you try to construct a member (OrderStatus("invalid") raises ValueError). Use Literal when you are annotating function signatures that accept existing string constants you do not control. Use Enum when you own the constants and want runtime validation, iteration, and type-checker integration.
What is the functional Enum API?
Python’s enum module supports a functional syntax for creating enums dynamically: Status = Enum("Status", ["PENDING", "SHIPPED", "DONE"]). This creates the same Status enum class as the class syntax, but is useful when the members are not known until runtime — for example, loading status names from a database or config file. Values are assigned starting from 1. You can also pass a dict to control values: Enum("Color", {"RED": "#FF0000", "GREEN": "#00FF00"}). The functional API produces real enum classes, not just dicts, so all the normal enum features (iteration, comparison, methods) work as expected.
Conclusion
Python enums replace the most common source of silent bugs — magic strings and integers — with named, type-checked constants that are safe to compare, iterate, and serialize. We covered the four most important types: base Enum for named constants with strict comparison, IntEnum and StrEnum for interoperability with external systems, and Flag for combinable permission and option sets. We saw how auto() removes the need to manually assign values, how enums integrate with match statements for exhaustive pattern matching, and how to enforce valid state transitions using enum-keyed transition tables.
The next step is to replace the oldest magic constants in your codebase. Start with any field that stores a status, state, type, or category as a raw string or integer — those are the high-value targets. Add @enum.unique to catch accidental value duplication, and add a classmethod for loading from external sources. For the full reference, the Python enum documentation covers every detail including functional API, mixin classes, and data enums added in Python 3.12.
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: