@dataclass¶
The @dataclass decorator automatically generates boilerplate methods for data-holding classes: constructor (__init__), equality (__eq__, __hash__), and string representation (__repr__).
Basic Usage¶
This generates a class with:
- Auto-properties for each field
- A constructor accepting all fields as parameters
- __eq__ (value equality via Equals and operator ==/!=)
- __hash__ (via HashCode.Combine)
- __repr__ (via ToString()), rendering each field with repr() as python does (Point(x=1.0, y=2.0), a str field quoted)
C# output:
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public Point(double x, double y)
{
this.X = x;
this.Y = y;
}
public override bool Equals(object? obj)
{
if (obj is not Point other)
return false;
return Equals(X, other.X) && Equals(Y, other.Y);
}
public override int GetHashCode() => HashCode.Combine(X, Y);
public static bool operator ==(Point? left, Point? right) => Equals(left, right);
public static bool operator !=(Point? left, Point? right) => !Equals(left, right);
public override string ToString() => $"Point(x={(global::Sharpy.Builtins.Repr(X))}, y={(global::Sharpy.Builtins.Repr(Y))})";
}
Field Declarations¶
All fields must have explicit type annotations:
Rules:
- Fields without default values must precede fields with default values (same as Python)
- Fields without type annotations are not recognized as dataclass fields
- Static fields (marked with @static) are excluded from dataclass synthesis
Field Defaults¶
A field may declare a default value. A compile-time-constant default (a number, string,
True/False, an enum member, or a const whose own initializer is constant) is emitted as the
default value of the corresponding constructor parameter.
A mutable-collection default -- a list, dict, or set literal; a list()/dict()/set()
call; or a comprehension -- is initialized per instance. Each object gets its own fresh
collection, so mutating one instance's field never affects another, and an explicit argument still
wins:
@dataclass
class Bag:
label: str
items: list[int] = [1]
def main() -> None:
a = Bag("a")
b = Bag("b")
a.items.append(2)
print(a.items) # [1, 2]
print(b.items) # [1] -- b is unaffected
print(Bag("c", [9]).items) # [9] -- an explicit argument wins
This differs from Python, where a bare mutable default raises ValueError at class definition and
the idiom is field(default_factory=...). Sharpy accepts the bare literal and gives it
default_factory semantics automatically.
Generated .NET shape. The synthesized constructor takes a nullable sentinel parameter and builds a fresh collection when the caller omits the argument:
public Bag(string label, Sharpy.List<int>? items = null)
{
this.Label = label;
this.Items = items ?? new Sharpy.List<int>() { 1 };
}
The per-instance value is assigned before __post_init__ runs, so the hook observes the
default (matching Python's default_factory-then-__post_init__ ordering):
@dataclass
class Box:
items: list[int] = [1]
count: int = 0
def __post_init__(self) -> None:
self.count = len(self.items)
def main() -> None:
print(Box().count) # 1
Nullable-typed fields keep the refusal. A mutable-collection default on a field whose type is
itself nullable is refused with SPY0400: the nullable sentinel the per-instance lowering relies
on would be indistinguishable from a genuine None. Use None as the default and build the
collection in __post_init__:
@dataclass
class Bad:
xs: list[int] | None = [1] # error[SPY0400]: Mutable default value is not allowed
# for field 'xs' in dataclass 'Bad'.
Parameters¶
The @dataclass decorator accepts keyword arguments:
| Parameter | Default | Description |
|---|---|---|
frozen |
False |
If True, generates { get; init; } properties instead of { get; set; } |
eq |
True |
If True, generates Equals, GetHashCode, operator ==, operator != |
repr |
True |
If True, generates ToString() |
With frozen=True, properties use init accessors:
__post_init__ Hook¶
Define __post_init__ to run custom logic after the generated constructor:
@dataclass
class Square:
side: float
area: float = 0.0
def __post_init__(self):
self.area = self.side * self.side
The generated constructor calls PostInit() after assigning all fields:
public Square(double side, double area = 0.0d)
{
this.Side = side;
this.Area = area;
PostInit(); // calls __post_init__
}
frozen=True refuses a __post_init__ assignment. A frozen field is emitted as a C#
init-only property, which C# allows to be set only inside an object initializer or directly in the
constructor's own body — __post_init__ runs as a separate method the constructor calls, not as a
statement inside it. Assigning self.<field> there (or in any other method) is refused with
SPY0706 instead of reaching Roslyn; python3's analog is FrozenInstanceError:
@dataclass(frozen=True)
class Square:
side: float
area: float = 0.0
def __post_init__(self) -> None:
self.area = self.side * self.side # error[SPY0706]: Cannot assign to field 'area'
# of frozen dataclass 'Square'; remove frozen=True
# or compute the value before construction
Inheritance¶
Dataclass inheritance collects fields from parent classes (parent fields first):
The child constructor accepts all fields and delegates inherited fields via base():
The generated Equals, GetHashCode, and ToString include all fields (inherited + own).
Explicit Method Override¶
When a class has an explicit __init__, the dataclass skips constructor generation but still generates __eq__, __hash__, and __repr__ (unless disabled via parameters):
@dataclass
class Named:
first: str
last: str
def __init__(self, full_name: str):
parts = full_name.split(" ")
self.first = parts[0]
self.last = parts[1]
Similarly, explicitly defining __eq__, __hash__, or __repr__ prevents the dataclass from generating those methods.
Disabling Features¶
Use eq=False to prevent equality synthesis:
This class gets a constructor and ToString but no Equals, GetHashCode, or operator ==/!=.
Restrictions¶
@dataclasscan only be applied to classes (not structs or interfaces) -- error SPY0380- Fields without defaults cannot follow fields with defaults -- error SPY0381
- All dataclass fields must have type annotations -- error SPY0382
- Decorator arguments must be boolean literals (
True/False) for recognized options (frozen,eq,repr) -- error SPY0383 - A mutable-collection default on a nullable-typed field is refused -- error SPY0400 (see Field Defaults)
- A non-constant, non-mutable-collection default (a tuple literal, or an arbitrary function call) is refused -- error SPY0401