Type guards for the Resolute package
We will look how type guards brought some quality of life improvements to working within the IDE. Then we will have a look at the Result pattern in action.
The Result pattern is a way to model operations that can either succeed or fail without relying on exceptions for control flow. Instead of catching exceptions at various call sites, every operation returns a typed value that carries either the result or the error — and the caller decides explicitly how to handle each case.
In the resolute package, this is expressed through three types: Resolute[T] as the base, and Success[T] and Failure[T] as its concrete subtypes. The factory methods handle construction:
from resolute import Resolute, Success, Failure
result = Resolute.from_value(42) # returns Success[int]
failure = Resolute.from_error("oops") # returns Failure[str]
This works fine at runtime, but MyPy still sees the declared return type as Success[int] and will not automatically narrow a variable typed as Resolute[int] just because you checked .is_success. That is where TypeIs comes in.
Type Guards with TypeIs
TypeIs[T] (introduced in PEP 742) lets you write a function that returns a bool, but tells the type checker that a True result means the argument is of a specific type. Unlike the older TypeGuard, TypeIs also narrows the type in the else branch, which is exactly what you want when you have a union like Success[T] | Failure[T].
The package ships two guards:
from resolute import is_success, has_errors, Resolute
def process(result: Resolute[int]) -> str:
if is_success(result):
# result is narrowed to Success[int] here
# result.value is T, not T | None
return f"Got {result.value}"
else:
# result is narrowed to Failure[int] here
return f"Failed: {result.concat_errors()}"
Without these guards, accessing .value on Resolute[T] returns T | None because the base class cannot guarantee the value is present. After narrowing to Success[T], the overridden .value property returns plain T — no assert, no cast, no # type: ignore at the call site.
The Result Alias
For annotating function return types, spelling out Success[T] | Failure[T] every time is verbose. The package exposes a generic type alias:
from resolute import Result
def divide(a: int, b: int) -> Result[float]:
if b == 0:
return Resolute.from_error(ValueError("division by zero"))
return Resolute.from_value(a / b)
Result[T] is declared using the type statement added in Python 3.12:
type Result[T] = Success[T] | Failure[T]
This is purely a type-level construct with no runtime overhead beyond the alias object itself. MyPy and Pyright treat it as a proper generic alias and will enforce that only Success[T] or Failure[T] instances satisfy it.
Putting It Together
A realistic usage pattern after all these additions:
from resolute import Resolute, Result, is_success, has_errors
def fetch_user(user_id: int) -> Result[dict]:
if user_id <= 0:
return Resolute.from_error(ValueError(f"Invalid id: {user_id}"))
return Resolute.from_value({"id": user_id, "name": "Alice"})
result = fetch_user(1)
if has_errors(result):
print(result.concat_errors())
# type checker knows that result is Success[dict]
# type checker knows value is dict, not dict | None
print(result.value["name"])
Let's hope these type guards make our package even more safe and intuitive to use with wide linter support.
