Python None Cannot Represent an Unloaded Field
Python makes absence easy to express. A database NULL, a missing JSON key, an
empty relation, and an optional function result can all become None.
That convenience becomes dangerous for partially loaded entities. If a query
did not select task.name, returning None tells business code something the
runtime does not know.
TeaQL's generated Python E expressions keep Value, Null, and
NotLoaded separate. Loaded nulls evaluate to None; unselected fields raise
a structured TeaQLNotLoadedError.
Two Objects That Look the Same
Consider these two tasks:
from models.task import Task
partial = Task(id=99)
loaded_null = Task(id=100, name=None)
Both have task.name is None, but their constructors received different
evidence. The first record contained only id. The second explicitly contained
name with a null value.
The generated model remembers the keys that were present, so E expressions can preserve the distinction:
from E import E, TeaQLNotLoadedError
assert E.task(loaded_null).name().eval() is None
try:
E.task(partial).name().eval()
raise AssertionError("expected NotLoaded")
except TeaQLNotLoadedError as error:
assert error.access_path == "name"
assert error.break_point == "name"
Why or_else Must Still Fail
It is tempting to make every access defensive:
label = E.task(task).name().or_else("Unnamed")
This fallback is correct when the query selected name and the value is NULL.
It is incorrect when the query never selected name.
TeaQL therefore propagates NotLoaded through or_else. A fallback is a business
rule for legitimate absence, not a substitute for loading the required data.
Typed Async Query Results
Raw row dictionaries are useful for reporting and aggregation, but E expressions operate on generated models with load-state metadata. Generated requests now provide typed async methods for this purpose:
tasks = await (
Q.tasks()
.with_name_is("Hello")
.comment("Read the task for display")
.purpose("Render the task detail page")
.execute_entities_for_list(ctx, service)
)
name = E.task(tasks[0]).name().eval()
execute_entities_for_list and execute_entity_for_one construct models from
the returned row keys. A key with value None is loaded Null. A key absent from
the row is NotLoaded.
The explicit .comment(...) and .purpose(...) calls remain part of the query
contract; typed mapping does not bypass TeaQL's audit requirements.
What the Error Contains
TeaQLNotLoadedError exposes both Python attributes and a structured details
payload:
except TeaQLNotLoadedError as error:
print(error.details)
A diagnostic includes:
error: TeaQLNotLoadedError
root: Task(id=99)
access_path: [name]
break_point: name
missing_preload: [name]
suggested_fix: select_name(...)
severity: error
For nested relations, the access path grows with the expression chain. The
failure therefore points to the first missing preload instead of surfacing later
as an AttributeError on None.
Why This Works Well with AI Coding
AI agents are much more reliable when a failure describes an exact local
contract. A generic NoneType has no attribute ... leaves several possibilities:
bad fixture data, a nullable business field, a missing relation, or a query that
did not preload enough data.
The three-state error removes that ambiguity. During a unit or integration test, the agent can:
- read the root and broken access path;
- inspect the generated request methods;
- add the suggested field or relation selection;
- rerun the test;
- retain the test as a regression check for query completeness.
This is a practical form of AI-native framework design: generated APIs constrain the solution, and generated diagnostics explain how the constraint was violated.
Verification Evidence
The generated Python workspace was compiled and executed with its asyncio-style
runtime. Tests covered loaded values, a loaded None, a partial entity,
fallback propagation, foreign-key IDs, reverse lists, and typed entity query
mapping.
The cross-language generator regression completed with 109 tests, zero failures, and zero errors. Database tests requiring connection variables remained conditional; separately, Java and .NET integration tests verified the same SQL NULL versus NotLoaded contract against real database readers.
None remains the right result for a legitimately absent value. TeaQL simply
refuses to use it as a disguise for data the program never loaded.
