MISSING Constant

The MISSING constant is similar to None, but with a key difference. While None has the type NoneType and can only be assigned to fields of type T | None, the MISSING constant has the type Any and can be assigned to fields of any type.

Motivation

The MISSING constant addresses a common issue when working with optional fields in configurations. Consider the following example:

import nshconfig as C
from typing import Annotated

# Without MISSING:
class MyConfigWithoutMissing(C.Config):
    age: int
    age_str: str | None = None

    def __post_init__(self):
        if self.age_str is None:
            self.age_str = str(self.age)

config = MyConfigWithoutMissing(age=10)
age_str_lower = config.age_str.lower()
# ^ The above line is valid code, but the type-checker will complain
# because `age_str` could be `None`.

In the above code, the type-checker will raise a complaint because age_str could be None. This is where the MISSING constant comes in handy:

Using MISSING with AllowMissing

There are two syntaxes for using the MISSING constant with the AllowMissing feature:

Syntax 1: Using Annotated

# With MISSING (Annotated syntax):
class MyConfigWithMissing(C.Config):
    age: int
    age_str: Annotated[str, C.AllowMissing] = C.MISSING

    def __post_init__(self):
        if self.age_str is C.MISSING:
            self.age_str = str(self.age)

config = MyConfigWithMissing(age=10)
age_str_lower = config.age_str.lower()
# ^ No more type-checker complaints!

Syntax 2: Direct AllowMissing Type

A more concise syntax is available using AllowMissing directly as a generic type:

# With MISSING (direct AllowMissing syntax):
class MyConfigWithMissing(C.Config):
    age: int
    age_str: C.AllowMissing[str] = C.MISSING

    def __post_init__(self):
        if self.age_str is C.MISSING:
            self.age_str = str(self.age)

config = MyConfigWithMissing(age=10)
age_str_lower = config.age_str.lower()
# ^ No type-checker complaints with this syntax either!

This second syntax is more concise and follows modern Python typing patterns.

By using either syntax with the MISSING constant, you can indicate that a field is not set during construction, and the type-checker will not raise any complaints.

Validating No Missing Values

If you want to ensure that your configuration doesn’t contain any MISSING values after initialization, you can call the model_validate_no_missing() method:

class MyConfig(C.Config):
    required_field: int
    optional_field: C.AllowMissing[str] = C.MISSING  # Using the direct syntax

    def __post_init__(self):
        # Set default values, etc.
        if self.optional_field is C.MISSING:
            self.optional_field = "default value"

        # Ensure no MISSING values remain
        self.model_validate_no_missing()

# This will work fine:
config1 = MyConfig(required_field=10)

# This would raise a validation error if model_validate_no_missing()
# wasn't setting a default value in __post_init__:
config1.optional_field  # "default value"

The model_validate_no_missing() method is useful when you want to ensure all fields have proper values before using your configuration.

Using AllowMissing with Complex Types

The AllowMissing feature works seamlessly with complex types like lists, dictionaries, and nested configurations:

class NestedConfig(C.Config):
    value: C.AllowMissing[str] = C.MISSING

class ParentConfig(C.Config):
    name: str
    nested: C.AllowMissing[NestedConfig] = C.MISSING
    items: C.AllowMissing[list[int]] = C.MISSING
    settings: C.AllowMissing[dict[str, float]] = C.MISSING

# Create with MISSING values
config = ParentConfig(name="test")
assert config.nested is C.MISSING
assert config.items is C.MISSING
assert config.settings is C.MISSING

# Assign values later
config.items = [1, 2, 3]
config.settings = {"scale": 1.5, "offset": 0.1}

This flexibility allows for dynamic configuration patterns where some values might not be available at initialization time.