'No module named pydantic_core._pydantic_core' in Docker or Lambda Is an Architecture Mismatch
Your FastAPI app runs perfectly on your machine. You build a slim Docker image or zip it up for AWS Lambda, deploy, and the first request dies before your code even runs:
ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'
So you do the obvious thing. You pip install pydantic-core again. You add it to requirements.txt explicitly. You pip show pydantic-core and it says, plainly, Version: 2.x, Location: /var/task. It is installed. The module it cannot import is inside a package that is right there. And yet.
Stop reinstalling. This is almost never a missing package. It is a compiled binary built for the wrong CPU architecture. pydantic-core is written in Rust and ships as a platform-specific compiled wheel. If the wheel that got installed was built for your dev machine (say Apple Silicon arm64, or a newer glibc) and you deployed it to an x86_64 Lambda or a slim container with an older glibc, the package files exist but the .so inside cannot load. Python reports that as "no module named", which sends everyone hunting for the wrong bug.
The _pydantic_core module is a compiled Rust extension shipped per-platform. The error means the installed wheel does not match the target architecture or glibc, not that the package is missing. Fix it by installing wheels for the target platform: build inside a container matching production (docker build --platform linux/amd64), or for Lambda run pip install --platform manylinux2014_x86_64 --only-binary=:all: --target ./package .... Never copy a virtualenv built on macOS or arm64 into an x86_64 image.
The exact error, and its close cousins
ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'
The same root cause surfaces for any Rust or C extension dependency, so you may instead see:
ImportError: /var/task/pydantic_core/_pydantic_core.abi3.so: cannot open shared object file
ImportError: ... : ELF load command address/offset not properly aligned
ImportError: cannot import name '_pydantic_core' from partially initialized module
and the identical class of failure hits cryptography, asyncpg, orjson, numpy, and psycopg[binary]. If the fix below works for one, it is the fix for all of them, because they all ship compiled wheels.
Why "it's installed" and "it can't import" are both true
A pure-Python package is just .py files; it runs anywhere Python runs. pydantic-core is not that. Its hot path is compiled Rust, distributed as a binary wheel whose filename encodes exactly what it was built for, for example:
pydantic_core-2.x-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
pydantic_core-2.x-cp312-cp312-macosx_11_0_arm64.whl
Those tags are not decoration. manylinux_2_17_x86_64 means "Intel/AMD 64-bit, glibc 2.17 or newer". macosx_11_0_arm64 means "Apple Silicon macOS". When you run pip install, pip picks the wheel matching the machine it is running on. Install on your M-series Mac and pip grabs the arm64 macOS wheel. That wheel contains a .so compiled for arm64 Darwin.
Now you copy that installed package into a Linux x86_64 image, or zip it for Lambda's x86_64 runtime. The pydantic_core/ directory is present. pip show reads its metadata and cheerfully reports it installed. But when Python tries to import pydantic_core._pydantic_core, it goes to dlopen the .so, the dynamic loader sees a binary for the wrong CPU or a glibc that does not exist on this system, and the load fails. Python's import machinery turns a failed extension load into ModuleNotFoundError: No module named 'pydantic_core._pydantic_core', which is maddeningly indistinguishable from the package genuinely being absent.
The fix
Step 1: Confirm it really is an architecture mismatch
Inside the failing environment (the container, or a Lambda test), look at the actual binary:
python -c "import pydantic_core; print(pydantic_core.__file__)"
file $(python -c "import pydantic_core, os; print(os.path.dirname(pydantic_core.__file__))")/_pydantic_core*.so
file will print something like ELF 64-bit LSB ... x86-64 or Mach-O 64-bit ... arm64. If it says arm64 or Mach-O on an x86_64 Linux box, that is your bug in one line. A missing file would fail the file command; a present-but-wrong file is the mismatch.
Step 2 (Docker): build on the target platform, not your laptop
The classic mistake is building an image on an Apple Silicon Mac with plain docker build, which produces an arm64 image, then running it on x86_64 infrastructure. Pin the platform so pip resolves the correct wheels:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# Build for the architecture you deploy to, regardless of your laptop's CPU
docker buildx build --platform linux/amd64 -t myapp:latest --load .
The key is that pip install runs inside the target-platform build, so it fetches the manylinux_x86_64 wheel. Do not pip install on the host and COPY a venv or site-packages into the image. That is the single most common way this bug is introduced. Build dependencies in the image, on the image's platform. If Docker is still new to you, my practical Docker on-ramp covers why the build context and base image matter this much.
Step 2 (AWS Lambda): install target wheels explicitly
For a Lambda zip or layer, tell pip to fetch wheels for Lambda's platform even while you run pip on a different machine:
pip install \
--platform manylinux2014_x86_64 \
--implementation cp \
--python-version 3.12 \
--only-binary=:all: \
--target ./package \
-r requirements.txt
--only-binary=:all: is doing real work here: it forbids pip from falling back to a source distribution and building a wheel for your local machine. If a target-platform wheel does not exist, you want a hard failure now, not a silently mis-built binary later. If your Lambda uses arm64 (Graviton), swap the tag to manylinux2014_aarch64. Even simpler and less error-prone: build the package inside an official AWS build image such as public.ecr.aws/sam/build-python3.12, which is Lambda-compatible by construction.
Step 3: watch the multi-stage glibc trap
A multi-stage Dockerfile can reproduce this even when both stages are x86_64, if the builder and runtime have different glibc versions. Compiling a wheel against a newer glibc in the builder and copying it into an older-glibc runtime (for example a -slim vs a distroless base from a different Debian generation) gives you the same import failure. Keep the builder and runtime on the same base image family, and prefer copying installed packages that came from manylinux wheels over locally-compiled ones.
Verification
Prove the import works in the real target before you deploy:
# Inside the built image, on the deploy architecture
docker run --rm --platform linux/amd64 myapp:latest \
python -c "import pydantic_core._pydantic_core; print('ok')"
Expected output is simply:
ok
If that prints ok inside a container run with the same --platform you deploy to, the wheel matches and the app will start. For Lambda, run the same one-line import against the ./package directory in a container of the target base image before you zip it.
What people get wrong
"Reinstall pydantic-core, it must be corrupted." Reinstalling on the same wrong-architecture machine downloads the same wrong-architecture wheel. You will do this five times and get five identical failures. pip show saying "installed" is precisely the trap; installed and loadable are different claims.
"Add build-essential and gcc to the final image so it compiles." This bloats a slim image back to fat and usually does not even help, because the failure is a mismatched pre-built wheel, not a missing compiler. If you truly need to compile, do it in a throwaway builder stage and copy only the resulting wheel into a clean runtime. Never ship gcc in production layers.
"Pin an older pydantic to avoid the Rust core." Downgrading to Pydantic v1 to dodge pydantic-core trades a solvable packaging bug for a much larger migration and a pile of v1-versus-v2 differences. If you are on that migration for other reasons, mind the gotchas the official guide skips, but do not migrate versions just to sidestep an architecture flag.
When it is still broken
- Check the Python minor version too. Wheels are tagged per interpreter (
cp312vscp311). A wheel built for 3.11 will not import under 3.12. Match the exact minor version between build and run, not just the architecture. - Look for a stale layer cache. Docker may be reusing a cached
pip installlayer from before you set--platform. Rebuild with--no-cacheto be sure the install actually re-ran on the new platform. - Confirm nothing is shadowing the package. A local file or directory named
pydantic_corein your working directory, or a partial copy in the Lambda zip root, can shadow the real one. Checkpydantic_core.__file__points where you expect. - For asyncpg and cryptography specifically, the same
--platformand--only-binary=:all:approach applies verbatim; if any dependency has no manylinux wheel at all for your target, that is the one you must build in a matching builder stage.
The durable takeaway: a compiled wheel is a promise about a specific CPU and a specific libc. "It is installed" only means the files copied. Whether they run is decided by where they were built, and the fix is always to build them where they will run.
Frequently asked questions
- Why does pip show pydantic-core as installed but Python says No module named pydantic_core._pydantic_core?
- Because installed and loadable are different things. pydantic-core ships a compiled Rust extension, and pip installed a wheel built for the wrong CPU architecture or glibc version. pip show only reads metadata, so it reports the package present, but when Python tries to dlopen the .so it fails and reports the submodule as missing. It is an architecture mismatch, not a missing package.
- How do I fix the pydantic_core error in a Docker image built on an Apple Silicon Mac?
- Build the image for the architecture you deploy to, not your laptop's arm64. Use docker buildx build --platform linux/amd64 so that pip install runs inside the target platform and downloads the x86_64 manylinux wheel. Never pip install on the host and COPY a venv or site-packages into the image, since that ships the host's arm64 binaries.
- How do I install pydantic-core correctly for AWS Lambda?
- Tell pip to fetch wheels for Lambda's platform explicitly: pip install --platform manylinux2014_x86_64 --implementation cp --python-version 3.12 --only-binary=:all: --target ./package -r requirements.txt. The --only-binary=:all: flag forbids a local source build, so you fail fast if no compatible wheel exists. For arm64 Lambda use manylinux2014_aarch64, or build inside a public.ecr.aws/sam/build-python image.
- Does this same error affect cryptography, asyncpg, and numpy?
- Yes. Any dependency that ships a compiled C or Rust extension can fail the same way when installed for the wrong architecture or glibc. The fix is identical: install or build the wheels on the target platform using --platform and --only-binary=:all:, or build them inside a builder stage that matches your runtime base image.