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.

Type guards for the Resolute package
Photo by N I F T Y A R T ✍🏻 / Unsplash

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.

resolute
Implementation of the Result pattern similar to C# ErrorOr package