Skip to content

field() Function

The field() function provides fine-grained control over how individual fields are handled in dataclasses, including default values, factory functions, and metadata.


Basic field() Usage

from dataclasses import dataclass, field
from typing import List

@dataclass
class Person:
    name: str
    age: int = field(default=0)  # With default
    hobbies: List[str] = field(default_factory=list)  # Mutable default

person1 = Person("Alice")
person2 = Person("Bob", age=30)
person3 = Person("Charlie", age=25, hobbies=["reading"])

print(person1)  # Person(name='Alice', age=0, hobbies=[])
print(person2)  # Person(name='Bob', age=30, hobbies=[])

default_factory for Mutable Objects

from dataclasses import dataclass, field
from typing import List, Dict

@dataclass
class Config:
    name: str
    options: Dict[str, int] = field(default_factory=dict)
    tags: List[str] = field(default_factory=list)

config1 = Config("api")
config2 = Config("database")

# Each gets its own dictionary and list
config1.options['timeout'] = 30
config1.tags.append('important')

print(config1)  # Config(name='api', options={'timeout': 30}, tags=['important'])
print(config2)  # Config(name='database', options={}, tags=[])

Field Metadata and Exclusion

from dataclasses import dataclass, field, asdict

@dataclass
class User:
    name: str
    password: str = field(repr=False)  # Don't show in repr
    email: str = field(compare=False)  # Don't use in comparisons

user1 = User("alice", "secret123", "alice@example.com")
user2 = User("alice", "different", "alice@different.com")

print(user1)           # User(name='alice', email='alice@example.com')
print(user1 == user2)  # True (email not compared, password excluded)

Custom Metadata

from dataclasses import dataclass, field, fields

@dataclass
class Product:
    name: str = field(metadata={'description': 'Product name'})
    price: float = field(metadata={'currency': 'USD', 'min': 0.0})
    quantity: int = field(default=0, metadata={'unit': 'items'})

# Access metadata
for f in fields(Product):
    print(f"{f.name}: {f.metadata}")

Initialization Order and Init

from dataclasses import dataclass, field

@dataclass
class Example:
    required: str
    optional: str = field(default="default_value")
    # Fields with init=False aren't included in __init__
    computed: str = field(init=False)

    def __post_init__(self):
        self.computed = f"{self.required}_{self.optional}"

example = Example("test")
print(example)  # Example(required='test', optional='default_value', computed='test_default_value')

Compare and Hash Control

from dataclasses import dataclass, field

@dataclass
class Item:
    id: int
    name: str = field(compare=True)
    internal_state: dict = field(compare=False, repr=False)

item1 = Item(1, "widget", {})
item2 = Item(1, "widget", {'processed': True})

print(item1 == item2)  # True (internal_state not compared)
print(hash(item1))     # Works with hashable fields

Runnable Example: dataclass_fields_and_defaults.py

"""
TUTORIAL: Dataclass Fields and Defaults - Understanding Type Annotations
=========================================================================

In this tutorial, you'll learn the difference between three types of attributes
in a dataclass:

  1. Typed fields with no default: REQUIRED when creating instances
  2. Typed fields with default values: OPTIONAL when creating instances
  3. Class attributes without type hints: NOT dataclass fields

This distinction is crucial. Dataclasses use type hints to identify which
attributes should be instance fields managed by the generated __init__ method.
Attributes without type hints are treated as class attributes instead.

Understanding this hierarchy prevents confusing bugs where you accidentally
forget to make something a field or misunderstand why something behaves
differently than expected.
"""

from dataclasses import dataclass


# ============ Example 1: Declaring Dataclass Fields ============

if __name__ == "__main__":
    print("=" * 70)
    print("EXAMPLE 1: Three types of attributes in a dataclass")
    print("=" * 70)

    @dataclass
    class DemoDataClass:
        """Demonstrates the three ways to define attributes in a dataclass.

        Attributes:
            a (int): A required field - MUST be provided when creating an instance.
                     Has a type hint, so @dataclass treats it as an instance field.

            b (float): An optional field - Has a default value (1.1).
                       If not provided, instances will use the default.
                       @dataclass treats this as an instance field because it has
                       a type hint.

            c: NOT a dataclass field - This class attribute lacks a type hint.
               @dataclass ignores it. It's a regular class attribute shared by all
               instances, like a constant or helper attribute.
        """
        # Field 1: Required - has type hint, no default
        a: int           # <1> Required when creating instance

        # Field 2: Optional - has type hint and default value
        b: float = 1.1   # <2> Optional, defaults to 1.1

        # Not a field - no type hint, so treated as class attribute
        c = 'spam'       # <3> Class attribute, not instance field


    print(f"\nClass definition complete. Let's see what @dataclass generates:\n")


    # ============ Example 2: Creating Instances with Required Fields ============
    print("=" * 70)
    print("EXAMPLE 2: Creating instances - mixing required and optional fields")
    print("=" * 70)

    # Create instance with only required field
    instance1 = DemoDataClass(a=42)
    print(f"\ninstance1 = DemoDataClass(a=42)")
    print(f"  instance1.a = {instance1.a}")
    print(f"  instance1.b = {instance1.b}  (uses default)")
    print(f"  WHY? 'b' has default value 1.1, so it's optional")

    # Create instance with both fields
    instance2 = DemoDataClass(a=100, b=2.5)
    print(f"\ninstance2 = DemoDataClass(a=100, b=2.5)")
    print(f"  instance2.a = {instance2.a}")
    print(f"  instance2.b = {instance2.b}")

    # Try creating instance without required field
    print(f"\nAttempting: DemoDataClass(b=3.0)  # Missing required 'a'")
    try:
        broken = DemoDataClass(b=3.0)
        print(f"  ERROR: This should have failed!")
    except TypeError as e:
        print(f"  Result: TypeError: {e}")
        print(f"  WHY? 'a' has no default value, so it's required")


    # ============ Example 3: Class Attributes vs Instance Attributes ============
    print("\n" + "=" * 70)
    print("EXAMPLE 3: Understanding class attribute 'c' vs instance fields")
    print("=" * 70)

    print(f"\nThe 'c' attribute is NOT an instance field:")
    print(f"  - It lacks a type hint")
    print(f"  - @dataclass ignores it")
    print(f"  - It becomes a class attribute")

    # Class attribute - shared by all instances
    print(f"\nClass attribute (shared by all instances):")
    print(f"  DemoDataClass.c = '{DemoDataClass.c}'")

    # But you can still access it through instances
    print(f"\nAccessing class attribute through instances:")
    print(f"  instance1.c = '{instance1.c}'")
    print(f"  instance2.c = '{instance2.c}'")

    print(f"\nModifying the class attribute affects ALL instances:")
    original_c = DemoDataClass.c
    DemoDataClass.c = 'eggs'
    print(f"  DemoDataClass.c = 'eggs'  # Changed class attribute")
    print(f"  instance1.c = '{instance1.c}'  (reflected immediately)")
    print(f"  instance2.c = '{instance2.c}'  (reflected immediately)")

    # Restore for clean output
    DemoDataClass.c = original_c
    print(f"  DemoDataClass.c = '{original_c}'  # Restored")

    print(f"\nWhy not make 'c' a dataclass field?")
    print(f"  - Without type hint, @dataclass doesn't know it's a field")
    print(f"  - This is intentional: class attributes are class-level constants")
    print(f"  - If you want instance field, add type hint: c: str = 'spam'")


    # ============ Example 4: Comparing with Proper Type Hints ============
    print("\n" + "=" * 70)
    print("EXAMPLE 4: What happens if we add a type hint to 'c'")
    print("=" * 70)

    @dataclass
    class ImprovedDemo:
        """Same as DemoDataClass but with 'c' as a proper typed field."""
        a: int
        b: float = 1.1
        c: str = 'spam'  # Now it has a type hint!


    instance3 = ImprovedDemo(a=50)
    print(f"\ninstance3 = ImprovedDemo(a=50)")
    print(f"  instance3.a = {instance3.a}")
    print(f"  instance3.b = {instance3.b}  (uses default 1.1)")
    print(f"  instance3.c = '{instance3.c}'  (uses default 'spam')")

    instance4 = ImprovedDemo(a=60, b=3.3, c='ham')
    print(f"\ninstance4 = ImprovedDemo(a=60, b=3.3, c='ham')")
    print(f"  instance4.a = {instance4.a}")
    print(f"  instance4.b = {instance4.b}")
    print(f"  instance4.c = '{instance4.c}'")

    print(f"\nNow 'c' is a real instance field:")
    print(f"  - Each instance has its own 'c' value")
    print(f"  - You can provide 'c' when creating instances")
    print(f"  - Changing instance4.c doesn't affect instance3.c")

    instance4.c = 'turkey'
    print(f"\ninstance4.c = 'turkey'")
    print(f"  instance4.c = '{instance4.c}'")
    print(f"  instance3.c = '{instance3.c}'  (unchanged)")


    # ============ Example 5: Generated __init__ Signature ============
    print("\n" + "=" * 70)
    print("EXAMPLE 5: The __init__ method @dataclass generates")
    print("=" * 70)

    import inspect

    print(f"\nDemoDataClass.__init__ signature:")
    sig = inspect.signature(DemoDataClass.__init__)
    print(f"  {sig}")
    print(f"\nExplanation:")
    print(f"  - self: Always first parameter")
    print(f"  - a: int - REQUIRED (no default shown)")
    print(f"  - b: float = 1.1 - OPTIONAL (default shown)")
    print(f"  - Note: 'c' is NOT in the signature (it's not a field)")

    print(f"\nImprovedDemo.__init__ signature:")
    sig2 = inspect.signature(ImprovedDemo.__init__)
    print(f"  {sig2}")
    print(f"\nNow 'c' appears in the signature because we added a type hint!")


    # ============ Example 6: Field Order Matters ============
    print("\n" + "=" * 70)
    print("EXAMPLE 6: Important - fields with defaults must come after required")
    print("=" * 70)

    print(f"\nDataclass rule: Required fields must come BEFORE optional fields")
    print(f"\nThis is valid:")
    @dataclass
    class ValidOrder:
        required_field: int          # No default
        optional_field: str = 'default'  # Has default

    print(f"  class ValidOrder:")
    print(f"      required_field: int")
    print(f"      optional_field: str = 'default'")

    print(f"\nThis would be INVALID:")
    print(f"  class BadOrder:")
    print(f"      optional_field: str = 'default'  # Has default")
    print(f"      required_field: int              # No default (ERROR!)")
    print(f"\nWhy? Python's parameter rules require defaults after non-defaults")

    print(f"\n" + "=" * 70)