Records and Pattern Matching
C# record types vs Python dataclasses, switch expressions vs match
Introduction
In this lesson, you'll learn about records and pattern matching in C#. Coming from Python, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Python, you're familiar with c# record types vs python dataclasses, switch expressions vs match.
C# has its own approach to c# record types vs python dataclasses, switch expressions vs match, which we'll explore step by step.
The C# Way
Let's see how C# handles this concept. Here's a typical example:
// record: immutable, value equality, auto ToString
public record Point(double X, double Y);
// 'with' expression — like dataclasses.replace()
var p1 = new Point(1, 2);
var p2 = p1 with { X = 5 }; // Point { X = 5, Y = 2 }
// Mutable record (record class vs record struct)
public record class MutablePoint
{
public double X { get; set; }
public double Y { get; set; }
}
// Sealed hierarchy for exhaustive matching
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rect(double W, double H) : Shape;
// switch expression with property patterns
string Describe(Shape shape) => shape switch
{
Circle { Radius: > 10 } c => $"large circle r={c.Radius}",
Circle c => $"circle r={c.Radius}",
Rect r => $"rect {r.W}x{r.H}",
_ => "unknown"
};
// Positional pattern
bool IsOrigin(Point p) => p is (0, 0);Comparing to Python
Here's how you might have written similar code in Python:
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
@dataclass(frozen=True) # immutable
class ImmutablePoint:
x: float
y: float
# "Copy with change" via replace
from dataclasses import replace
p1 = ImmutablePoint(1, 2)
p2 = replace(p1, x=5) # ImmutablePoint(5, 2)
# match/case (Python 3.10+)
def describe(shape):
match shape:
case {"type": "circle", "radius": r} if r > 10:
return f"large circle r={r}"
case {"type": "rect", "w": w, "h": h}:
return f"rect {w}x{h}"
case _:
return "unknown"You may be used to different syntax or behavior.
record is immutable by default with value equality — like @dataclass(frozen=True)
You may be used to different syntax or behavior.
'with' expression creates modified copy — equivalent to dataclasses.replace()
You may be used to different syntax or behavior.
switch expression (=>) returns a value; each arm ends with , not :
You may be used to different syntax or behavior.
Property patterns ({ Prop: value }) match and deconstruct in one expression
You may be used to different syntax or behavior.
sealed + abstract record enables exhaustive switch (compiler checks all cases)
Step-by-Step Breakdown
1. Records
A record auto-generates constructor, property accessors, Equals (structural), GetHashCode, and ToString. Perfect for data transfer objects.
@dataclass
class Point:
x: float
y: floatrecord Point(double X, double Y);
// Equivalent to:
// constructor Point(double X, double Y)
// Equals: compares X and Y values
// ToString: "Point { X = 1, Y = 2 }"2. with Expression
The 'with' expression creates a shallow copy of a record with specified properties changed — exactly like dataclasses.replace().
from dataclasses import replace
p2 = replace(p1, x=5)var p2 = p1 with { X = 5 };
// p1 is unchanged — records are immutable3. Switch Expressions
Switch expressions are concise and return values. Combine with type patterns to replace long if/elif chains.
match value:
case 1: return "one"
case 2: return "two"
case _: return "other"string result = value switch {
1 => "one",
2 => "two",
_ => "other"
};4. Property and Type Patterns
Property patterns check values inside { }. Combine type pattern + property pattern for expressive matching.
match shape:
case Circle(radius=r) if r > 10:
return f"large"string r = shape switch {
Circle { Radius: > 10 } c => $"large circle r={c.Radius}",
Circle c => $"small circle r={c.Radius}",
_ => "other"
};Common Mistakes
When coming from Python, developers often make these mistakes:
- record is immutable by default with value equality — like @dataclass(frozen=True)
- 'with' expression creates modified copy — equivalent to dataclasses.replace()
- switch expression (=>) returns a value; each arm ends with , not :
Key Takeaways
- record = @dataclass(frozen=True) — immutable, value equality, auto-generated members
- 'with' expression = dataclasses.replace() — non-destructive update
- switch expression returns values; property patterns { Prop: value } deconstruct inline
- sealed + record hierarchy enables exhaustive matching — compiler catches missing cases