Gabor Hosszu
09/16/2025, 10:45 AMawait step.state.result()
where step
is just a flow_run.
Please note that this code has not change in months and was working perfectly fine until today, what could it be?
Traceback (most recent call last):
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/_result_records.py", line 199, in deserialize
instance = cls.model_validate_json(data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/pydantic/main.py", line 746, in model_validate_json
return cls.__pydantic_validator__.validate_json(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for ResultRecord
Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value=b'gAWV6QAAAAAAAAB9lIwHY29...yaXZhdGVfX5ROdWJzLg==\n', input_type=bytes]
For further information visit <https://errors.pydantic.dev/2.11/v/json_invalid>
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/client/schemas/objects.py", line 241, in aresult
return await get_state_result(
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/states.py", line 85, in get_state_result
return await _get_state_result(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/states.py", line 160, in _get_state_result
result = await _get_state_result_data_with_retries(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/states.py", line 117, in _get_state_result_data_with_retries
record = await ResultStore._from_metadata(state.data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/results.py", line 392, in _from_metadata
result = await store.aread(metadata.storage_key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/results.py", line 676, in aread
return await self._read(key=key, holder=holder, _sync=False)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/results.py", line 633, in _read
result_record: ResultRecord[Any] = ResultRecord.deserialize(
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/_result_records.py", line 204, in deserialize
result = backup_serializer.loads(data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/gabor/workspace/sweap/lead-companion-backend/.venv/lib/python3.12/site-packages/prefect/serializers.py", line 149, in loads
return pickler.loads(base64.decodebytes(blob))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'src'
Marvin
09/16/2025, 10:45 AMMarvin
09/16/2025, 10:50 AMsrc
(e.g., something like src.my_module.MyClass
). When Prefect unpickles the result, Python needs to import that module to reconstruct the object. If the environment where you’re calling await step.state.result()
does not have src
on PYTHONPATH
(or the package installed properly), unpickling will fail.
Why it worked before and broke “today”
- Most often this is environment drift:
- The place you’re calling .result()
from changed (new shell, container, or machine) and no longer has PYTHONPATH
including your repo root or src
.
- Your worker or local venv was rebuilt without installing your project package (e.g., missing pip install -e .
).
- Changes to how you launch code (e.g., different working directory) removed src
from the import path.
- Nothing in your code needs to have changed for this to happen; a subtle environment/path change is enough.
Ways to fix/unblock
1) Make the src
package importable wherever you call `.result()`:
- Preferable: install your project as a package so imports don’t depend on src
being on the path.
pip install -e .
and import using your package name (not src...
).
- Or temporarily add it to the path when running:
export PYTHONPATH=$PWD
# or if your code is under ./src, export PYTHONPATH=$PWD/src
Or in Python:
import sys, os
sys.path.append(os.path.abspath("src")) # or repo root depending on your layout
2) Future-proof: avoid persisting pickled objects
- If your results are JSON-serializable (dicts, lists, strings, numbers), switch to JSON for results. This avoids cross-environment import issues entirely.
- Set at the flow level:
from prefect import flow
from prefect.serializers import JSONSerializer
@flow(result_serializer=JSONSerializer())
def my_flow():
return {"ok": True}
- Or set globally:
export PREFECT_RESULTS_DEFAULT_SERIALIZER=json
- If you need to persist non-JSON objects (e.g., DataFrames), consider writing them to storage (parquet/csv) and returning the URI/path instead of the object itself.
3) If you must stick with Pickle
- Ensure any returned classes are defined in a proper, importable module that is installed in every environment that reads the results.
- Avoid returning objects from modules only reachable via ad-hoc PYTHONPATH=src
hacks.
Notes
- Existing runs that already persisted pickled results will still require the same import path to be available to deserialize. Switching to JSON will protect future runs, not retroactively fix old results.
- The initial “Invalid JSON” message is expected — Prefect tries JSON first, then falls back to the configured/backup serializer.
Helpful references
- Prefect serializers API: Serializers API
- Results overview: Results in Prefect
- Source code for serializers: prefect/serializers.py
A few quick questions to pinpoint the exact cause for you
- Where are you calling await step.state.result()
from (local dev shell, notebook, another service), and is that different from before?
- Do you import your own code via src.*
anywhere or rely on PYTHONPATH=src
? What’s your project layout?
- Did you recently rebuild a container/venv or upgrade Python/Prefect?Marvin
09/16/2025, 10:50 AMGabor Hosszu
09/16/2025, 11:48 AMMarvin
09/16/2025, 11:50 AMMarvin
09/16/2025, 11:51 AMfrom prefect import flow
from prefect.serializers import JSONSerializer
@flow(result_serializer=JSONSerializer())
def my_flow():
model = MyModel(...)
return model.model_dump() # Pydantic v2
# or: return model.dict() # Pydantic v1
Or set globally:
export PREFECT_RESULTS_DEFAULT_SERIALIZER=json
If you want it automatic for Pydantic models
- Use a custom JSON encoder that converts BaseModel to dict, then defers to Prefect’s default for everything else.
1) Create an encoder in your project (important: make it importable):
# my_project/serialization.py
from pydantic import BaseModel
from prefect.serializers import prefect_json_object_encoder
def pydantic_first_encoder(obj):
if isinstance(obj, BaseModel):
dump = getattr(obj, "model_dump", None) # v2
return dump() if dump else obj.dict() # v1 fallback
return prefect_json_object_encoder(obj)
2) Use it in your flow:
from prefect import flow
from prefect.serializers import JSONSerializer
@flow(result_serializer=JSONSerializer(
object_encoder="my_project.serialization.pydantic_first_encoder"
))
def my_flow():
return MyModel(...)
This will persist results as JSON dicts instead of pickled objects, avoiding the import-path problem entirely. You’ll get dicts back when you call await state.result()
.
Why this helps
- Pickle requires the exact class to be importable at load time (hence the failure on src.*
). JSON avoids that by storing plain data.
- Existing runs that already stored pickled results will still need the original module import path to be available; the change helps all future runs.
Docs
- Serializers API: Serializers API
- Results guide: Results in Prefect
If you can share your Pydantic version (v1 or v2) and how you import your project (package vs. src
path), I can tailor the encoder or recommend the smallest change to your flows.