Skip to content

Python 3.14 a6/a7 Compatibility Issue with httpx 0.28.1 and typing.Union #3548

Closed as duplicate

Description

@codebydivine

When using httpx 0.28.1 with Python 3.14.0 (alpha 6 and alpha 7), an AttributeError occurs:

7112109dc9 failed: 'typing.Union' object has no attribute '__module__' and no __dict__ for setting new attributes
divine-core-1  | Traceback (most recent call last):
divine-core-1  |   File "/app/divine/divine/networking/core.py", line 71, in request
divine-core-1  |     async with httpx.AsyncClient(http2=True, verify=TLS_CONTEXT_HTTP2) as session:
divine-core-1  |                ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpx/_client.py", line 1402, in __init__
divine-core-1  |     self._transport = self._init_transport(
divine-core-1  |                       ~~~~~~~~~~~~~~~~~~~~^
divine-core-1  |         verify=verify,
divine-core-1  |         ^^^^^^^^^^^^^^
divine-core-1  |     ...<5 lines>...
divine-core-1  |         transport=transport,
divine-core-1  |         ^^^^^^^^^^^^^^^^^^^^
divine-core-1  |     )
divine-core-1  |     ^
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpx/_client.py", line 1445, in _init_transport
divine-core-1  |     return AsyncHTTPTransport(
divine-core-1  |         verify=verify,
divine-core-1  |     ...<4 lines>...
divine-core-1  |         limits=limits,
divine-core-1  |     )
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpx/_transports/default.py", line 294, in __init__
divine-core-1  |     import httpcore
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpcore/__init__.py", line 140, in <module>
divine-core-1  |     setattr(__locals[__name], "__module__", "httpcore")  # noqa
divine-core-1  |     ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
divine-core-1  | AttributeError: 'typing.Union' object has no attribute '__module__' and no __dict__ for setting new attributes. Did you mean: '__reduce__'?

Issue Analysis

The problem occurs in the httpcore package (a dependency of httpx) in its init.py file (around line 140). The code attempts to set the module attribute on all exported names:

7112109dc9 failed: 'typing.Union' object has no attribute '__module__' and no __dict__ for setting new attributes
divine-core-1  | Traceback (most recent call last):
divine-core-1  |   File "/app/divine/divine/networking/core.py", line 71, in request
divine-core-1  |     async with httpx.AsyncClient(http2=True, verify=TLS_CONTEXT_HTTP2) as session:
divine-core-1  |                ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpx/_client.py", line 1402, in __init__
divine-core-1  |     self._transport = self._init_transport(
divine-core-1  |                       ~~~~~~~~~~~~~~~~~~~~^
divine-core-1  |         verify=verify,
divine-core-1  |         ^^^^^^^^^^^^^^
divine-core-1  |     ...<5 lines>...
divine-core-1  |         transport=transport,
divine-core-1  |         ^^^^^^^^^^^^^^^^^^^^
divine-core-1  |     )
divine-core-1  |     ^
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpx/_client.py", line 1445, in _init_transport
divine-core-1  |     return AsyncHTTPTransport(
divine-core-1  |         verify=verify,
divine-core-1  |     ...<4 lines>...
divine-core-1  |         limits=limits,
divine-core-1  |     )
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpx/_transports/default.py", line 294, in __init__
divine-core-1  |     import httpcore
divine-core-1  |   File "/usr/local/lib/python3.14t/site-packages/httpcore/__init__.py", line 140, in <module>
divine-core-1  |     setattr(__locals[__name], "__module__", "httpcore")  # noqa
divine-core-1  |     ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
divine-core-1  | AttributeError: 'typing.Union' object has no attribute '__module__' and no __dict__ for setting new attributes. Did you mean: '__reduce__'?

In Python 3.14, typing.Union objects can be included in the module namespace, but they don't have a module attribute and don't support setting attributes (no dict).

Environment

  • Python 3.14.0 alpha 6 and alpha 7
  • httpx 0.28.1
  • httpcore 1.0.7

Steps to Reproduce

  • Install Python 3.14.0 (alpha 6 or 7)
  • Install httpx 0.28.1
  • Try to use httpx.AsyncClient with http2=True

Suggested Fix

The httpcore module should check if the object supports attribute setting before attempting to set the module attribute. Something like:

__locals = locals()
for __name in __all__:
    if not __name.startswith("__"):
        setattr(__locals[__name], "__module__", "httpcore")  # noqa

This should be a relatively simple fix that maintains backward compatibility with older Python versions while supporting Python 3.14.

Activity

  1. codebydivine commented on Apr 10, 2025

    @codebydivine
    Author

    Hi team,

    Following up on the Python 3.14 compatibility issue (AttributeError: 'typing.Union' object has no attribute '__module__') originating from httpcore/__init__.py when used with httpx, we've developed a temporary workaround for our codebase until an upstream fix is available.

    The issue occurs because httpcore attempts to set __module__ on objects from its __all__ list, which fails for types like typing.Union on Python 3.14+.

    Our workaround uses a sys.meta_path import hook to patch httpcore during the import process. It allows the initial import error to occur but then manually applies the problematic setattr loop with proper error handling.

    Workaround Implementation:

    1. Create the patch file (divine/networking/patch_httpcore.py):

      """
      Monkey patch for httpcore using sys.meta_path hook.
      Fixes Python 3.14 compatibility issue with typing.Union in httpcore <= 1.0.7.
      
      This patch should be imported as early as possible, before any other imports
      that might trigger the import of httpx or httpcore.
      """
      import importlib.util
      import os
      import sys
      from importlib.abc import MetaPathFinder
      from importlib.machinery import ModuleSpec, SourceFileLoader
      
      # --- Define the Loader (handles execution) ---
      class PatchedHttpcoreLoader(SourceFileLoader):
          def exec_module(self, module):
              """Execute the original module, catch the specific error, then patch."""
              try:
                  # Let the standard loader execute the module code
                  super().exec_module(module)
              except AttributeError as e:
                  # Check if it's the specific error we want to handle
                  if "'typing.Union' object has no attribute '__module__'" in str(e):
                      # The module is partially loaded in module.__dict__
                      # Now, manually run the problematic loop safely
                      self._manual_patch_loop(module)
                  else:
                      # Different AttributeError, re-raise
                      print(f"ERROR: Unexpected AttributeError during httpcore execution: {e}", file=sys.stderr)
                      raise
              except Exception as e:
                   # Other unexpected errors during execution
                   print(f"ERROR: Unexpected error during httpcore execution: {e}", file=sys.stderr)
                   raise
      
          def _manual_patch_loop(self, module):
              """Manually run the problematic loop from httpcore.__init__ safely."""
              module_locals = getattr(module, "__locals", None)
              all_list = getattr(module, "__all__", None)
      
              if all_list is not None and module_locals is not None:
                   for name in all_list:
                       if not name.startswith("__"):
                           if name in module_locals:
                               try:
                                   obj = module_locals[name]
                                   obj.__module__ = "httpcore"
                               except (AttributeError, TypeError):
                                   pass # Expected for typing.Union etc.
                               except Exception as e_inner:
                                   # Log unexpected errors during the loop itself
                                   print(f"ERROR: Unexpected error in _manual_patch_loop for {name}: {e_inner}", file=sys.stderr)
                           # Silently skip names from __all__ not in __locals
              # else: # Optionally log if __all__ or __locals were missing
                   # print("ERROR: __all__ or __locals not found in httpcore module during manual patch attempt.", file=sys.stderr)
      # --- End Loader Definition ---
      
      
      # --- Define the Finder (intercepts import) ---
      class HttpcoreMetaFinder(MetaPathFinder):
          _original_httpcore_origin = None
      
          def find_spec(self, fullname, path, target=None):
              """Return a spec using our custom loader if fullname is 'httpcore'."""
              if fullname == "httpcore":
                  if self._original_httpcore_origin is None:
                      try:
                          original_spec = importlib.util.find_spec(fullname)
                          if original_spec and original_spec.origin:
                              self._original_httpcore_origin = original_spec.origin
                          else:
                              # Cannot proceed without origin
                              return None
                      except Exception as e:
                          print(f"ERROR: HttpcoreMetaFinder error finding original spec: {e}", file=sys.stderr)
                          return None
      
                  if self._original_httpcore_origin:
                       package_dir = os.path.dirname(self._original_httpcore_origin)
                       spec = ModuleSpec(
                           fullname,
                           PatchedHttpcoreLoader(fullname, self._original_httpcore_origin),
                           origin=self._original_httpcore_origin,
                           is_package=True,
                       )
                       spec.submodule_search_locations = [package_dir]
                       return spec
                  else:
                      return None
      
              return None
      # --- End Finder Definition ---
      
      
      # --- Installation Logic ---
      # Only apply the patch for Python 3.14 and above
      if sys.version_info >= (3, 14):
          # Check if httpcore is already imported (shouldn't be if this runs early enough)
          if "httpcore" in sys.modules:
              print("ERROR: httpcore already in sys.modules! sys.meta_path patch cannot be applied effectively.", file=sys.stderr)
          else:
              # Check if our finder is already installed
              finder_installed = any(isinstance(f, HttpcoreMetaFinder) for f in sys.meta_path)
              if not finder_installed:
                  # Install the finder at the beginning of the meta path
                  sys.meta_path.insert(0, HttpcoreMetaFinder())
      # else: # Optionally log when patch is skipped
          # print("DEBUG: Python version < 3.14, patch not needed.")
    2. Import the patch VERY early: In the main application entry point (e.g., cli.py for us), add these lines at the absolute top, before any other imports:

      # Apply httpcore patch VERY early for Python 3.14+ compatibility
      import sys
      if sys.version_info >= (3, 14):
          try:
              # Import the patch module for its side effects (installs the hook)
              import divine.networking.patch_httpcore # noqa: F401
          except Exception as _patch_exc:
              # Log or print error if patch fails, but allow app to continue
              # (it will likely fail later if the patch was needed)
              print(f"CRITICAL: Failed to apply httpcore patch: {_patch_exc}", file=sys.stderr)
      
      # --- Rest of your application's imports and code ---
      # ...
  2. zanieb commented on Apr 11, 2025

    @zanieb
    Contributor

    Thanks for the report.

    This is a duplicate of encode/httpcore#995

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions