Feature Group Resolution Errors
Resolution runs inside a full mloda request, but you do not need to build one
to reproduce a resolution error. resolve_feature (from mloda.provider) runs
the same matcher over the same candidate universe as a run and reports what a
run would, without raising: the failure lands in result.error instead of an
exception, so for the matching failures on this page the message below is the
message you get back.
from mloda.provider import resolve_feature
result = resolve_feature("my_feature") # or resolve_feature(Feature(...)) for a full request
if result.error:
print(result.error) # for a matching failure, the same message a run raises
print([fg.__name__ for fg in result.candidates])
One failure is reported rather than echoed: a plugin that raises while declaring
its frameworks fails the environment build, which resolve_feature prefixes with
Failed to build the plugin environment: where a run raises it bare. See
Broken framework declarations.
Its candidate universe is a standalone default, not a replay of a specific run.
Pass a Feature, options, plugin_collector, links,
data_access_collection, or compute_frameworks (a run that restricts its
frameworks needs this to mirror) to reproduce the run you are debugging. See
Discover Plugins for the full signature.
Catching resolution failures
Inside a run, a feature that does not resolve to exactly one FeatureGroup raises
FeatureResolutionError during planning (the mloda.run_all(...) /
mloda.prepare(...) call, before any compute runs). It is a ValueError
subclass, so existing except ValueError handlers keep working, but you can now
catch it specifically and inspect why it failed:
from mloda.provider import FeatureResolutionError
from mloda.user import mloda
try:
mloda.run_all(["my_feature"])
except FeatureResolutionError as err:
print(err.feature_name) # the feature that failed to resolve
print(err) # the human-readable message a run prints
for record in err.partial_records:
print(record.feature_name, record.requested) # features resolved before the failure
# err.result carries the per-candidate elimination facts (an EvaluationResult)
To inspect a request without catching an exception, use the non-raising
surfaces: resolve_feature for a single name (above), or mlodaAPI.diagnose(...),
which returns a ResolutionDiagnosis for the whole request. See
mlodaAPI for both, plus the
fields on FeatureResolutionError, ResolutionDiagnosis, and ResolutionRecord.
No Feature Groups Found Error
The Problem
FeatureResolutionError: No feature groups found for feature name: 'sales_revnue'. Requested domain: 'marketing'.
Feature group(s) eliminated while matching 'sales_revnue':
- MarketingRevenueGroup (compute framework): none of its compute frameworks are enabled for this run
- SalesFeatureGroup (domain): declares domain 'sales', but the run requested 'marketing'
Did you mean one of: ['sales_revenue']?
Use resolve_feature(name, options=...) to debug feature resolution.
For troubleshooting guide, see: https://mloda-ai.github.io/mloda/in_depth/troubleshooting/feature-group-resolution-errors/
No enabled feature group both declared the requested name and survived every matching gate. It is raised as FeatureResolutionError during planning; see Catching resolution failures to inspect it, or follow the message's closing pointer and rerun the request through resolve_feature. Pass the same Feature and run arguments, as the top of this page describes; the bare name alone would drop the domain and default to all frameworks, changing the outcome.
The eliminated candidates block
Each line names a candidate the matcher considered and dropped: the first gate that eliminated it (the parenthesized label) and that gate's reason. A line does not prove the candidate declared the requested name; a candidate whose match hook raised or whose input-data gate declined is recorded regardless.
| Label | What eliminated the candidate | Typical fix |
|---|---|---|
option value |
The group declined an option value in the request. | Fix the value the reason names. |
input data |
The input-data gate declined the request. | Point the request at data the group can read (Data Access Patterns). |
match hook |
The group's match hook raised; the error is contained and quoted. | Fix the plugin bug it names. |
domain |
The group declares a different domain than the request. | Align the requested domain (domain solution below). |
scope |
The group is outside the requested feature_group scope. |
Widen or correct the scope (scope solution below). |
compute framework |
None of the group's compute frameworks are usable: its capability hook rejected every enabled framework, or none of its frameworks is enabled for the run. | Enable a framework the group supports. |
compute framework pin |
The Feature pins compute_frameworks to one that is not among the group's supported set for this run. |
Change or drop the pin. |
links |
No index column of the group matches the run's links. | Align the run's links with the group's index. |
The Did you mean hint
Suggestions are close matches drawn from the catalog of accessible groups: declared feature names, class names, and class-name prefixes. The hint drops what would only repeat or mislead: the requested name itself, names the eliminated-candidates block already covers, and names only unreachable groups declare (abstract, no enabled framework, or outside the requested domain, scope, or links). A missing hint therefore means no reachable group declares anything close. In the example above, 'sales_revenue' survives because a third, still-reachable group declares it; the two eliminated candidates could not have contributed it.
Only abstract feature group bases matched
A sibling variant fires when an abstract base matched the name and no concrete group won:
No feature groups found for feature name: 'my_feature'. Only abstract feature group base(s) matched, which cannot be instantiated; no concrete implementation is available or enabled.
Use resolve_feature(name, options=...) to debug feature resolution.
For troubleshooting guide, see: https://mloda-ai.github.io/mloda/in_depth/troubleshooting/feature-group-resolution-errors/
or, when concrete implementations exist but their compute frameworks do not:
No feature groups found for feature name: 'my_feature'. Its concrete implementations require compute framework(s) ['PandasDataFrame'], none of which are available or enabled for this run.
Use resolve_feature(name, options=...) to debug feature resolution.
For troubleshooting guide, see: https://mloda-ai.github.io/mloda/in_depth/troubleshooting/feature-group-resolution-errors/
For the first shape, import or enable a concrete implementation. For the second, enable one of the named compute frameworks; the list comes from the base's accessible concrete implementations, so check that the one you enable actually serves the name. Eliminated candidates, if any, still render in their block, and the message closes with the same pointer lines as the ordinary form. Only the Did-you-mean suggestion is dropped, because the name already matched a base.
Multiple Feature Groups Error
The Problem
FeatureResolutionError: Multiple feature groups found for feature '<feature_name>':
- FeatureGroupA (...) [domain: ...]
- FeatureGroupB (...) [domain: ...]
This error occurs when multiple distinct feature groups claim they can handle the same feature. Each feature must resolve to exactly one feature group to prevent conflicts. It is raised as FeatureResolutionError (a ValueError subclass) during planning; see Catching resolution failures to inspect it.
If you previously hit this in a notebook because of a redefined feature group (re-running a cell that defines class MyFG(FeatureGroup): ...), that case is now auto-deduplicated. If this error still appears, it points to a real conflict between two distinct classes (different (module, qualname)).
Solutions
1. Use PluginCollector to enable or disable feature groups
Control which feature groups are loaded to prevent conflicts:
from mloda.user import PluginCollector
# Disable specific conflicting feature groups
collector = PluginCollector.disabled_feature_groups({ConflictingFeatureGroupA, ConflictingFeatureGroupB})
# Or enable only specific feature groups you need
collector = PluginCollector.enabled_feature_groups({RequiredFeatureGroupA, RequiredFeatureGroupB})
2. Avoid loading all plugins
You can load plugins by importing the module as a class.
But you can also import just plugins from subfolders.
from mloda.user import PluginLoader
plugin_loader = PluginLoader()
plugin_loader.load_group("feature_group") # load plugins only from mloda_plugins.feature_group
3. Use Domains to Separate Feature Groups
from mloda.user import Domain
@classmethod
def get_domain(cls):
return Domain("sales") # Makes this FG only handle 'sales' domain features
4. Scope a feature to one source (shared keys across sources)
When two enabled sources declare the same column (for example a shared join key), requesting that column by bare name is ambiguous. Scope the request to one source. The scope is read by feature resolution and by filter matching, and does not affect feature identity.
# class object, collision-proof
Feature("subject_token", feature_group=ClaimsReader)
# class-name string form, the only form a JSON config can carry
Feature("subject_token", feature_group="ClaimsReader")
The same scope in a JSON config (feature config):
[
{"name": "subject_token", "feature_group": "ClaimsReader"}
]
The config form takes the class-name string only, because JSON cannot express a class object; the class-object form is Python-only.
Both forms match the scoped class and its subclasses, preferring the most specific one. Naming an abstract family base therefore selects the concrete subclass, so a config can scope to the family without naming a compute-framework-specific class:
[
{"name": "age__mean_aggr", "feature_group": "AggregatedFeatureGroup"}
]
Caveats:
- The scope narrows candidates; it does not break ties between them. If the run enables two compute frameworks whose concrete subclasses both match, the family base stays ambiguous and raises, exactly as the bare name does. Enable one framework for the run, or pin
compute_frameworkson theFeature(Python only). - The class object is collision-proof; the string form is not. Two classes with the same name in different modules both match the string, so the request stays ambiguous and raises.
- Neither form pins one exact class: a subclass of the named class is preferred over it. To resolve to a specific implementation, name that implementation.
- The root
FeatureGroupbase is rejected in either form: it names no family. - The scope is read by feature resolution and by filter matching, and stays excluded from Feature identity, so two requests for the same column name scoped to different sources compare equal. Requesting both in one features list raises
ValueError: Duplicate feature setup: <name>rather than silently dropping one, so you are told, not surprised. Inside a singleinput_features()returning a set literal, a second same-name feature with a different scope is silently deduplicated by the Python set itself before the engine ever sees it, so never scope the same name twice within one feature group. To read the same column from two sources side by side, give them distinct derived feature names.
FeatureGroup Redefinition Errors
The Problem
ValueError: FeatureGroup redefined with different source code:
- MyFG (__main__) source hash 5a3f0c12
- MyFG (__main__) source hash b1e052a3
Set PluginCollector(...).set_allow_redefinition() to keep only the most recently defined version of each class.
If you are running this in a notebook, restart the kernel to clear stale class definitions.
This error occurs when the same feature group class name has been redefined with different source code in a long-lived Python namespace. Common triggers:
- Re-running a Jupyter cell that defines
class MyFG(FeatureGroup): ...after editing the class body. IPython'sOut[N]history holds a strong reference to the old class object, so it stays alive inFeatureGroup.__subclasses__(). - Calling
importlib.reloadon a module that defines feature group classes.
If both versions of the class have identical source code (e.g., re-running a cell without edits), mloda silently keeps one. The error fires only when the source actually differs.
Solutions
1. Take the most recent version (iterative development)
For rapid iteration in notebooks, opt in to "newest wins" using the builder method on PluginCollector:
Option A: set the override flag on a fresh collector:
from mloda.provider import FeatureGroup
from mloda.user import PluginCollector
class SomeFG(FeatureGroup):
pass
plugin_collector = PluginCollector().set_allow_redefinition()
Option B: compose with disable/enable filters:
plugin_collector = (
PluginCollector.disabled_feature_groups({SomeFG})
.set_allow_redefinition()
)
Pass this collector through to mloda.run_all, resolve_feature, or any other entry point that accepts a plugin_collector argument.
2. Restart the kernel
Restarting the Jupyter kernel clears Out[N] history and unloads all stale class objects. Use this when you want a fully clean state and do not need to preserve other notebook state.
EmptyResultError
The Problem
EmptyResultError: Result carries no schema (no columns): MyFeatureGroup. ...
This error fires when a final requested feature group returns a result with no schema (zero columns). There is no opt-in or opt-out: the rule applies uniformly on every compute framework. Note that zero rows is not an error: a well-typed frame with the right columns and no rows is a valid result and passes through. Only a result that carries no columns at all is rejected.
On the PythonDict framework, whose native representation is a columnar
dict[str, list], the schema is the set of keys: {"col": []} carries a
schema, while {} (zero columns) is the only schema-less value and raises.
Returning a row-oriented [] ("nothing to ingest": an empty source directory, a
query with no hits) also carries no columns and therefore raises. Return
{"my_feature": []} instead.
If the schema-less result is genuine (a bug, missing data, wrong filter), this error is correct behavior: it protects callers from silently receiving output they cannot interpret.
When to fix it
If an empty result is a legitimate answer for your domain, for example: graph traversals that may find no path, search queries that may match nothing, authorization filters that may deny all rows, or agent memory that may have no relevant entries, return a schema-bearing empty result: keep the columns and drop the rows. On PythonDict that is a dict with empty column lists; on the other frameworks it is an empty typed table or frame with the right columns.
class MyFeatureGroup(FeatureGroup):
@classmethod
def calculate_feature(cls, data, features):
...
return {"my_feature": []} # schema-bearing, zero rows: valid
A schema-bearing empty result flows through to the caller without raising. The
public import from mloda.provider import EmptyResultError (a ValueError
subclass) supports a typed except when you invoke framework or plugin code
directly. When calling mloda.run_all, worker errors are wrapped in a generic
Exception whose message embeds the original traceback, so match on the
EmptyResultError name in the raised exception's message instead.
Intermediates are exempt
The check applies only to features that were explicitly requested by the caller.
Intermediate feature groups, meaning those whose output feeds another feature
group rather than the final result, are never subject to EmptyResultError.
For full details on how the guard works and the schema-presence gate, see Empty Results.