I’ve spent countless hours writing boilerplate for data-holding classes. I remember the days of manually writing __init__, __repr__, and __eq__ methods for every single entity in a project.
It was tedious and often led to small, annoying bugs that were hard to track down during late-night deployments.
Then came Python 3.7 and the introduction of dataclasses, which completely changed how I structure my data models.
In this tutorial, I’ll show you how to use type hints and default values in Python dataclasses to write cleaner, more professional code.
Understand Type Hints in Dataclasses
In a standard Python class, type hints are optional and often ignored by the runtime.
However, in a dataclass, type hints are mandatory because the @dataclass decorator uses them to identify which fields to include.
If you define a variable without a type hint, the dataclass decorator will simply skip it, treating it as a regular class variable.
Let’s look at a practical example involving a real estate listing in the United States.
from dataclasses import dataclass
@dataclass
class PropertyListing:
address: str
city: str
state: str
zip_code: int
price: float
is_active: bool
# Creating an instance
home = PropertyListing(
address="1600 Pennsylvania Avenue NW",
city="Washington",
state="DC",
zip_code=20500,
price=0.0,
is_active=True
)
print(home)I executed the code above and added the screenshot below.

In this snippet, each field has a specific type hint, such as str or int.
This makes the code self-documenting and allows IDEs to provide better autocompletion for your American business applications.
How to Assign Simple Default Values
One of the best features of dataclasses is the ability to assign default values directly to fields.
This is incredibly useful when you have fields that usually stay the same, like a country code for a US-based shipping app.
When you provide a default value, that field becomes optional when you initialize the class.
from dataclasses import dataclass
@dataclass
class USShippingLabel:
recipient_name: str
street_address: str
city: str
state: str
weight_oz: float
carrier: str = "USPS" # Default value
country: str = "USA" # Default value
# Usage without specifying carrier or country
label = USShippingLabel(
recipient_name="John Doe",
street_address="742 Evergreen Terrace",
city="Springfield",
state="IL",
weight_oz=12.5
)
print(label)I executed the code above and added the screenshot below.

In my experience, placing defaults at the end is crucial because Python requires non-default arguments to come before default ones.
If you try to put a field without a default after one with a default, Python will throw a TypeError.
Use field() for Advanced Default Values
Sometimes a simple value isn’t enough, and you need more control over how a field behaves.
I often use the field() function from the dataclasses module when I want to hide a field from the repr or exclude it from comparisons.
For instance, if you are tracking employee data for a New York-based firm, you might want to exclude a sensitive ID from the printed output.
from dataclasses import dataclass, field
@dataclass
class NYCEmployee:
name: str
department: str
salary: float
employee_id: str = field(default="PENDING", repr=False)
emp = NYCEmployee("Alice Smith", "FinTech", 125000.0, "NYC-9982")
print(emp) # The employee_id won't show up in the print statementThis level of granularity is what makes dataclasses so much more powerful than named tuples or standard classes for data-heavy tasks.
Handle Mutable Default Values with default_factory
A common mistake I see junior developers make is trying to use a list or a dictionary as a direct default value.
In Python, mutable defaults are shared across all instances, which can lead to disastrous data leaks between objects.
Dataclasses solve this elegantly with the default_factory argument inside the field() function.
Imagine you are building a system to track sports stats for the NFL. You want each player to have a list of weekly scores.
from dataclasses import dataclass, field
from typing import List
@dataclass
class NFLPlayerStats:
player_name: str
team: str
# Incorrect: scores: List[int] = []
# Correct way using default_factory:
scores: List[int] = field(default_factory=list)
player1 = NFLPlayerStats("Patrick Mahomes", "Chiefs")
player1.scores.append(28)
player2 = NFLPlayerStats("Joe Burrow", "Bengals")
print(f"{player2.player_name} scores: {player2.scores}") # This will be an empty list, as expectedI executed the code above and added the screenshot below.

Using default_factory=list ensures that every new NFLPlayerStats object gets its own fresh list in memory.
Type Hinting for Optional and Union Fields
In real-world US business logic, data isn’t always simple. A field might be optional or accept multiple types.
I rely heavily on the typing module, specifically Optional and Union, to make my dataclasses robust.
If you’re processing tax forms like a W-2, certain fields might be missing depending on the filing status.
from dataclasses import dataclass
from typing import Optional, Union
@dataclass
class TaxFiling:
taxpayer_id: str
total_income: float
# Middle name might not exist
middle_name: Optional[str] = None
# Deduction could be a flat amount (float) or a category code (str)
deduction: Union[float, str] = 0.0
filing = TaxFiling(taxpayer_id="123-45-6789", total_income=85000.50)
print(filing)This approach helps static type checkers like Mypy catch errors before you even run your code, saving you hours of debugging.
Enforce Type Hints at Runtime
It is important to remember that Python’s type hints are mostly for documentation and static analysis.
By default, dataclasses won’t stop you from passing a string into an integer field during runtime.
If I’m working on a critical financial application, say, for a Wall Street trading platform, I often add a __post_init__ method.
This method runs automatically after the __init__ is finished, allowing you to validate data types.
@dataclass
class StockTrade:
symbol: str
shares: int
price: float
def __post_init__(self):
if not isinstance(self.shares, int):
raise TypeError(f"Shares must be an integer, got {type(self.shares)}")
if self.shares <= 0:
ValueError("Cannot trade zero or negative shares")
# This will trigger the validation
try:
trade = StockTrade("AAPL", "Ten", 150.0)
except TypeError as e:
print(f"Validation Error: {e}")Work with Class-Level Variables
Sometimes you need a variable that is shared by all instances of a dataclass, but you don’t want it to be treated as a field.
In this case, I use the ClassVar type hint. This tells the dataclass decorator to ignore the variable during initialization.
This is perfect for constants like a state-specific sales tax rate in a retail application.
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class CaliforniaRetailItem:
item_name: str
base_price: float
# Shared constant for all items
SALES_TAX: ClassVar[float] = 0.0725
def get_total(self) -> float:
return self.base_price * (1 + self.SALES_TAX)
item = CaliforniaRetailItem("Laptop", 1200.00)
print(f"Total price in CA: ${item.get_total():.2f}")Compare Type Hints and Default Values in Dataclasses
When you’re designing your data structures, it’s helpful to see how these features interact.
Below is a quick reference table based on my experience with high-scale Python projects.
| Feature | Syntax | Best Use Case |
| Basic Type Hint | field: type | Mandatory fields (e.g., Name, ID). |
| Simple Default | field: type = value | Constant-like strings or numbers. |
| Mutable Default | field: type = field(default_factory=list) | Lists, Sets, or Dicts. |
| Hidden Field | field: type = field(repr=False) | Passwords or sensitive tokens. |
| Constant | field: ClassVar[type] = value | Global settings for the class. |
Using these correctly will ensure your code follows the “Pythonic” way and remains maintainable as your project grows.
In this tutorial, I’ve covered the essential ways to use default values and type hints in Python dataclasses.
Whether you’re building a simple script or a complex enterprise application for the US market, these tools are indispensable.
The more you practice using these structures, the more natural they will feel in your daily coding workflow.
You may also like to read:
- Python Aggregation vs Composition
- How to Build a Simple OOP Project in Python
- Python Composition vs Inheritance
- Python Dataclass vs. Normal Class

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.