An Azure service that provides an event-driven serverless compute platform.
Python imports the single discoverable aiohttp package directory; the stale .dist-info directories do not select which code is imported, but they can make metadata-based version reporting ambiguous and should be removed by rebuilding the complete .python_packages tree and redeploying it.
What Python loads
import aiohttp follows Python’s normal import resolution over sys.path. It loads the first matching module or package—here, the relevant aiohttp/__init__.py—without using aiohttp-*.dist-info to choose a version.^1^
The .dist-info directories describe installed distributions. Tools such as importlib.metadata discover and read them, but distribution names and import-package names do not necessarily have a one-to-one relationship.^2^ They commonly contain the version, dependency declarations, installed-file records, and entry points.^3^
Consequently:
- The code under
aiohttp/determines the imported implementation. -
aiohttp.__file__identifies the code location. -
aiohttp.__version__, if exposed by that release, is supplied by the imported code. -
importlib.metadata.version("aiohttp")reads distribution metadata and does not prove which implementation was imported. - Duplicate metadata is not a supported multi-version installation model. Metadata discovery should not be assumed to select the newest version; the result can depend on discovery order.^4^
- Therefore,
pip, dependency checks, entry-point discovery, or libraries using package metadata can report or act on the wrong stale record even thoughimport aiohttploads the current code.
Use straight quotes and double underscores when checking the running environment:
python - <<'PY'
import aiohttp
from importlib import metadata
print("Module file:", aiohttp.__file__)
print("Code version:", getattr(aiohttp, "__version__", "<not exposed>"))
print("Metadata version:", metadata.version("aiohttp"))
for dist in metadata.distributions():
if (dist.metadata.get("Name") or "").lower() == "aiohttp":
print("Metadata:", dist.version, dist._path)
PY
Run this inside the Function App environment, because executing it on the pipeline agent examines the agent’s Python installation instead.
Clean recovery
Deleting the complete .python_packages directory and immediately deploying a clean artifact is preferable to deleting selected .dist-info directories. It avoids leaving a mixture of package code, metadata, compiled extensions, and dependency files from different builds.
- Stop the Function App or otherwise prevent requests during cleanup.
- Delete
/home/site/wwwroot/.python_packages. - Deploy the complete clean ZIP containing the rebuilt
.python_packages/lib/site-packages. - Restart the app and run the diagnostic above.
- Confirm that the module path is under the expected deployment and that only one
aiohttpdistribution is listed.
Risk: deleting .python_packages makes third-party imports unavailable until deployment completes. A deployment slot is safer where the hosting plan supports one.
For a local build, recreate the folder rather than installing over an existing target:
rm -rf .python_packages
python -m pip install \
--target="./.python_packages/lib/site-packages" \
--requirement requirements.txt
Azure Functions requires locally built dependencies in that directory, and the build environment should match the Linux production environment and Python version.^5^
ZIP deployment deletes or updates files tracked as part of an earlier deployment but can retain other files and directories. That behavior can explain why files created outside the tracked deployment set survive.^6^
Preventing recurrence
Prefer immutable Run From Package where your hosting plan supports it. It mounts the ZIP as read-only wwwroot, so the running tree exactly reflects the package instead of accumulating extracted files.^6^
With AzureFunctionApp@2, explicitly select it rather than relying on auto-detection:
- task: AzureFunctionApp@2
inputs:
azureSubscription: 'service-connection'
appType: 'functionAppLinux'
appName: 'function-app-name'
package: '$(Pipeline.Workspace)/drop/function.zip'
deploymentMethod: 'runFromPackage'
The task supports runFromPackage, which mounts the package instead of extracting it.^7^
Plan-specific constraints matter:
- Flex Consumption: already runs from a package; do not set
WEBSITE_RUN_FROM_PACKAGE. - Premium or Dedicated: set
WEBSITE_RUN_FROM_PACKAGE=1. - Linux Consumption:
1is unsupported; locally built packages require an external package URL. The Azure DevOps task also states that itsdeploymentMethodselection is unsupported for Linux Consumption. - Do not combine Run From Package with remote build; remote build instead requires its documented build settings.^6^
References
- importlib — The implementation of import — Python 3.15.2 documentation
- importlib.metadata – Accessing package metadata — Python 3.15.2 ...
- Recording installed projects - Python Packaging User Guide
- Migration guide - importlib_metadata 10.1.0 documentation
- Build your Python Azure Functions apps
- Package-based deployment for Azure Functions (dedicated-plan)
- AzureFunctionApp@2 - Azure Functions Deploy v2 task