Type-Safe Enums — Practice Tasks¶
Category: Resource & Type-Safety Patterns — model fixed sets as real types, with full Go, Java, Python solutions.
10 practice tasks with full Go, Java, Python solutions.
Table of Contents¶
- Task 1: Replace Int Constants With an Enum
- Task 2: Enum Carrying Associated Data
- Task 3: Strategy-Per-Constant
- Task 4: Parse External Input at the Boundary
- Task 5: Exhaustive Switch
- Task 6: Go Enum Done Right
- Task 7: Flag / Bitset Enum
- Task 8: Enum State Machine
- Task 9: Stable Serialization
- Task 10: Sum Type Where Cases Differ
Task 1: Replace Int Constants With an Enum¶
Refactor int status constants into a real type.
Java¶
// Before
static final int PENDING = 0, PAID = 1, SHIPPED = 2;
void handle(int status) { ... }
// After
public enum Status { PENDING, PAID, SHIPPED }
void handle(Status status) { ... }
Python¶
from enum import Enum, auto
class Status(Enum):
PENDING = auto()
PAID = auto()
SHIPPED = auto()
def handle(status: Status) -> None: ...
Go¶
type Status int
const (
Pending Status = iota
Paid
Shipped
)
func handle(status Status) { /* ... */ }
Task 2: Enum Carrying Associated Data¶
Model HTTP statuses with their numeric codes and a category.
Java¶
public enum HttpStatus {
OK(200), CREATED(201), NOT_FOUND(404), SERVER_ERROR(500);
private final int code;
HttpStatus(int code) { this.code = code; }
public int code() { return code; }
public boolean isError(){ return code >= 400; }
}
Python¶
from enum import Enum
class HttpStatus(Enum):
OK = 200
CREATED = 201
NOT_FOUND = 404
SERVER_ERROR = 500
@property
def is_error(self) -> bool:
return self.value >= 400
Go¶
type HttpStatus int
const (
OK HttpStatus = 200
Created HttpStatus = 201
NotFound HttpStatus = 404
ServerError HttpStatus = 500
)
func (h HttpStatus) IsError() bool { return h >= 400 }
Task 3: Strategy-Per-Constant¶
Each operation computes differently; adding one must force implementing it.
Java¶
public enum Operation {
PLUS { public double apply(double a, double b) { return a + b; } },
MINUS { public double apply(double a, double b) { return a - b; } },
TIMES { public double apply(double a, double b) { return a * b; } },
DIVIDE{ public double apply(double a, double b) { return a / b; } };
public abstract double apply(double a, double b);
}
double r = Operation.TIMES.apply(6, 7); // 42.0
Python¶
from enum import Enum
class Operation(Enum):
PLUS = "+"
MINUS = "-"
TIMES = "*"
def apply(self, a: float, b: float) -> float:
match self:
case Operation.PLUS: return a + b
case Operation.MINUS: return a - b
case Operation.TIMES: return a * b
Go¶
type Operation int
const ( Plus Operation = iota; Minus; Times )
func (o Operation) Apply(a, b float64) float64 {
switch o {
case Plus: return a + b
case Minus: return a - b
case Times: return a * b
default: panic("unhandled operation")
}
}
Task 4: Parse External Input at the Boundary¶
Convert raw strings into the enum once, rejecting invalid input.
Python¶
from enum import Enum
class Level(Enum):
DEBUG = "debug"; INFO = "info"; WARN = "warn"; ERROR = "error"
def parse_level(raw: str) -> Level:
try:
return Level(raw.lower())
except ValueError:
raise ValueError(f"unknown level {raw!r}")
Java¶
public enum Level {
DEBUG, INFO, WARN, ERROR;
public static Level parse(String raw) {
try {
return Level.valueOf(raw.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("unknown level: " + raw);
}
}
}
Go¶
func ParseLevel(raw string) (Level, error) {
switch strings.ToLower(raw) {
case "debug": return Debug, nil
case "info": return Info, nil
case "warn": return Warn, nil
case "error": return Error, nil
default: return 0, fmt.Errorf("unknown level %q", raw)
}
}
Task 5: Exhaustive Switch¶
Write a switch the compiler (or linter) checks for completeness.
Java (switch expression — exhaustive)¶
String label(Status s) {
return switch (s) { // no default: adding a constant won't compile
case PENDING -> "Awaiting payment";
case PAID -> "Paid";
case SHIPPED -> "Shipped";
};
}
Python (assert_never)¶
from typing import assert_never
def label(s: Status) -> str:
match s:
case Status.PENDING: return "Awaiting payment"
case Status.PAID: return "Paid"
case Status.SHIPPED: return "Shipped"
case _: assert_never(s) # mypy errors if a case is unhandled
Go (linter-enforced — no default)¶
// With //nolint discipline + the `exhaustive` linter, omit default:
func label(s Status) string {
switch s { //exhaustive:enforce
case Pending: return "Awaiting payment"
case Paid: return "Paid"
case Shipped: return "Shipped"
}
return "" // unreachable if linter passes
}
Task 6: Go Enum Done Right¶
Add the safety scaffolding Go's compiler omits.
package order
import "fmt"
type Status int
const (
Pending Status = iota
Paid
Shipped
statusEnd // unexported upper bound
)
//go:generate stringer -type=Status
func (s Status) Valid() bool { return s >= Pending && s < statusEnd }
func ParseStatus(raw string) (Status, error) {
switch raw {
case "Pending": return Pending, nil
case "Paid": return Paid, nil
case "Shipped": return Shipped, nil
default: return 0, fmt.Errorf("invalid status %q", raw)
}
}
stringer generates String(); Valid() guards bounds; CI runs the exhaustive linter.
Task 7: Flag / Bitset Enum¶
Model combinable permissions.
Java (EnumSet)¶
enum Perm { READ, WRITE, EXECUTE, DELETE }
EnumSet<Perm> perms = EnumSet.of(Perm.READ, Perm.WRITE);
boolean canWrite = perms.contains(Perm.WRITE);
perms.add(Perm.EXECUTE);
Python (enum.Flag)¶
from enum import Flag, auto
class Perm(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
p = Perm.READ | Perm.WRITE
assert Perm.READ in p
Go (bit shifts)¶
type Perm uint
const (
Read Perm = 1 << iota
Write
Execute
)
p := Read | Write
canWrite := p&Write != 0
Task 8: Enum State Machine¶
Encode legal transitions in the type.
Java¶
public enum State {
PENDING { public Set<State> next() { return EnumSet.of(PAID, CANCELLED); } },
PAID { public Set<State> next() { return EnumSet.of(SHIPPED); } },
SHIPPED { public Set<State> next() { return EnumSet.noneOf(State.class); } },
CANCELLED { public Set<State> next() { return EnumSet.noneOf(State.class); } };
public abstract Set<State> next();
public State to(State target) {
if (!next().contains(target))
throw new IllegalStateException(this + " -> " + target + " illegal");
return target;
}
}
Python¶
from enum import Enum
class State(Enum):
PENDING = "pending"; PAID = "paid"; SHIPPED = "shipped"; CANCELLED = "cancelled"
_TRANSITIONS = {
State.PENDING: {State.PAID, State.CANCELLED},
State.PAID: {State.SHIPPED},
State.SHIPPED: set(),
State.CANCELLED: set(),
}
def transition(cur: State, target: State) -> State:
if target not in _TRANSITIONS[cur]:
raise ValueError(f"{cur} -> {target} illegal")
return target
Task 9: Stable Serialization¶
Persist a stable code, never the ordinal.
Java¶
public enum Status {
PENDING("P"), PAID("A"), SHIPPED("S");
private final String code;
Status(String code) { this.code = code; }
public String code() { return code; }
private static final Map<String, Status> BY_CODE = new HashMap<>();
static { for (Status s : values()) BY_CODE.put(s.code, s); }
public static Status fromCode(String code) {
Status s = BY_CODE.get(code);
if (s == null) throw new IllegalArgumentException("unknown code: " + code);
return s;
}
}
Python¶
from enum import Enum
class Status(Enum):
PENDING = "P"; PAID = "A"; SHIPPED = "S"
@classmethod
def from_code(cls, code: str) -> "Status":
return cls(code) # value IS the stable code
db_value = Status.PAID.value # "A"
restored = Status.from_code(db_value) # Status.PAID
Go¶
var codeToStatus = map[string]Status{"P": Pending, "A": Paid, "S": Shipped}
var statusToCode = map[Status]string{Pending: "P", Paid: "A", Shipped: "S"}
func (s Status) Code() string { return statusToCode[s] }
func FromCode(c string) (Status, error) {
s, ok := codeToStatus[c]
if !ok { return 0, fmt.Errorf("unknown code %q", c) }
return s, nil
}
Task 10: Sum Type Where Cases Differ¶
When cases carry different data, promote past a flat enum.
Java (sealed + records, Java 21)¶
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
};
}
Python (dataclasses + match)¶
from dataclasses import dataclass
@dataclass(frozen=True)
class Circle: radius: float
@dataclass(frozen=True)
class Rectangle: w: float; h: float
Shape = Circle | Rectangle
def area(s: Shape) -> float:
match s:
case Circle(radius=r): return 3.14159 * r * r
case Rectangle(w=w, h=h): return w * h
Go (sealed interface)¶
type Shape interface{ isShape() }
type Circle struct{ R float64 }
type Rect struct{ W, H float64 }
func (Circle) isShape() {}
func (Rect) isShape() {}
func Area(s Shape) float64 {
switch v := s.(type) {
case Circle: return math.Pi * v.R * v.R
case Rect: return v.W * v.H
default: panic("unhandled shape")
}
}
Practice Tips¶
- Always parse external input into the enum at the boundary, then stay typed.
- Avoid
default:over your own closed enums so exhaustiveness checks keep working. - Persist names or explicit codes, never ordinals.
- In Go, pair every enum with
stringer,Valid(), and theexhaustivelinter. - When cases need different fields, switch from a flat enum to a sum type.
← Interview · Resource & Type-Safety · Roadmap · Next: Find-Bug
In this topic