Skip to content

Conversation

aatle
Copy link
Contributor

@aatle aatle commented Dec 1, 2024

A new approach to lazy loading pygame submodules after #3232 had a problem with slower attribute accesses (__getattr__).

Implementation explanation:
The implementation is twofold, where the second part has many alternatives.

First, pygame checks if numpy exists.
Then, each submodule is loaded if numpy exists and the submodule's other pygame module dependencies (e.g. mixer) load successfully. If numpy is missing or a module dependency fails, then the submodule becomes MissingModule. It is necessary to get an error to use its message in MissingModule.reason, so try-except is used.

If successful, the submodule is created and put into sys.modules, using the documented LazyLoader implementation, relying on import machinery.
This makes it so that the submodule exists, but its contents are not executed until the first attribute or method access on the submodule.
It does this by changing the module class temporarily to _LazyModule; after the module is actually loaded, the module class is set back to the normal module type.

Potential differences from current behavior:
The initial checks are not completely perfect. It is assumed that if numpy is importable, it will not raise an ImportError nor OSError.
Obviously, numpy not being loaded by pygame automatically is an intended difference. Instead, it is loaded upon the first attribute access of either submodule. I do not expect this to cause any lag spikes in-game: the user probably imports numpy themselves, and it will also load if directly importing one of the submodule functions, or using one during game loading.
After a submodule is loaded successfully, there should not be any noticeable difference from current behavior. The module class is the same, the original loader is put back, the module code was not changed.
https://docs.python.org/3/library/importlib.html#importlib.util.LazyLoader

Note: For projects where startup time is critical, this class allows for potentially minimizing the cost of loading a module if it is never used. For projects where startup time is not essential then use of this class is heavily discouraged due to error messages created during loading being postponed and thus occurring out of context.

There are no expected errors during postponed loading of these submodules, as they should have all been handled at the start.

Lazy loading of numpy has great potential benefits, so it's worth the extra complexity.

Closes #3232

Summary by CodeRabbit

  • Refactor

    • Array- and sound-related modules are now lazily loaded to avoid heavy imports at startup, improving startup time and memory use.
    • Modules load on first use and continue to work when optional dependencies are absent; packaging/distribution unaffected.
  • Documentation

    • Added version-changed notes documenting lazy loading behavior in the 2.5.6 release.

@aatle aatle requested a review from a team as a code owner December 1, 2024 23:36
@@ -23,6 +23,8 @@ Each sample is an 8-bit or 16-bit integer, depending on the data format. A
stereo sound file has two values per sample, while a mono sound file only has
one.

.. versionchanged:: 2.5.3 sndarray module is lazily loaded to avoid loading NumPy needlessly
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still doesn't explain why the action is actually being taken.

Why is it bad to load NumPy?

@Starbuck5
Copy link
Member

Starbuck5 commented Dec 6, 2024

@damusss You cast some doubt about this PR working with PyInstaller on discord but did not elaborate.

If there's a problem here can you explain what you did please.

@damusss
Copy link
Member

damusss commented Dec 6, 2024

If there's a problem here can you explain what you did please.

🫡 I tested both normal pygame-ce and this branch, and it looks like both imports are successful. when I got that error I must have been using an incomplete version of the pull request
image

@MyreMylar
Copy link
Member

MyreMylar commented Dec 31, 2024

Looks like this change has uncovered a pylint bug. Checking it locally it doesn't happen. Looks like a recurrence of this bug:

pylint-dev/pylint#8589

Which the last commenter said only happened on CI. Maybe there is some multithreaded shenanigans happening.

@aatle aatle requested a review from Starbuck5 March 13, 2025 06:31
Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to see some benchmarking/numbers on how much this PR is helping. Just so we can weigh in the added complexity/maintenance-burden against it. The benchmark should cover both:

  • A simple import pygame
  • getattr of affected submodules

@aatle
Copy link
Contributor Author

aatle commented Mar 31, 2025

Both are with numpy installed. (Without numpy or after loading numpy, times are the same.)

Simple import python -X importtime -c "import pygame"
import

First attribute access after importing pygame pygame.surfarray.array2d
access

(note: if accessing just pygame.surfarray, it is the same, because for lazy it doesn't get loaded, and for main it is already loaded.)

@ankith26
Copy link
Member

ankith26 commented Apr 8, 2025

These benchmarks makes sense, thanks for working on it. You mention "first attribute access" I wonder how the performance of subsequent attribute accesses are affected (if it is)

@aatle
Copy link
Contributor Author

aatle commented Apr 8, 2025

After the first attribute access, I'm fairly certain both have the same access times. (There is no module __getattr__, both modules are of type types.ModuleType.) So the modules should be indistinguishable afterwards.

@aatle aatle added the Performance Related to the speed or resource usage of the project label May 20, 2025
Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for the PR 🎉

@ankith26 ankith26 added this to the 2.5.4 milestone May 21, 2025
@Starbuck5 Starbuck5 removed this from the 2.5.4 milestone May 24, 2025
@ankith26 ankith26 added this to the 2.5.6 milestone Jun 7, 2025
@aatle
Copy link
Contributor Author

aatle commented Aug 14, 2025

@coderabbitai full review please

Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

@aatle I'll conduct a full review of this PR implementing the new lazy loading approach for pygame submodules.

✅ Actions performed

Full review triggered.

Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

Walkthrough

Adds lazy loading for pygame.surfarray and pygame.sndarray via a new lazy_import helper in src_py/init.py, defers NumPy import until first access, preserves MissingModule behavior when NumPy is absent, and documents the change in sndarray and surfarray reST files.

Changes

Cohort / File(s) Summary
Core lazy-loading implementation
src_py/__init__.py
Adds lazy_import(name) using importlib LazyLoader, introduces numpy_missing flag, initializes surfarray and sndarray as lazy imports when NumPy is available, retains MissingModule fallback when not, updates packager_imports() to explicitly import the lazy modules, and removes helper names from the public namespace.
Documentation updates
docs/reST/ref/sndarray.rst, docs/reST/ref/surfarray.rst
Inserts versionchanged directives noting that the modules are lazily loaded in version 2.5.6 to avoid unnecessary NumPy import; directive appears in both sndarray and surfarray docs at relevant API points.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Python
    participant pygame as pygame.__init__
    participant NumPy
    participant surfarray
    participant sndarray
    participant Packager

    User->>Python: import pygame
    Python->>pygame: execute initialization
    pygame->>NumPy: check availability (find_spec)
    alt NumPy present
        pygame->>pygame: set surfarray = lazy_import("surfarray")
        pygame->>pygame: set sndarray = lazy_import("sndarray")
    else NumPy missing
        pygame->>pygame: set surfarray = MissingModule
        pygame->>pygame: set sndarray = MissingModule
    end
    Packager->>pygame: scan source / call packager_imports()
    pygame->>Packager: contains explicit imports for surfarray/sndarray

    User->>pygame: access pygame.surfarray or import pygame.surfarray
    pygame->>surfarray: load module on first attribute access (triggers NumPy import inside module if needed)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement lazy loading for pygame.surfarray and pygame.sndarray (#3232)
Preserve MissingModule behavior when NumPy is unavailable (#3232)
Maintain compatibility across access methods (import, from, attribute access) (#3232) Implementation appears to support attribute and import access, but no tests or explicit handling for all from ... import ... forms are shown.
Ensure packaging tools (e.g., PyInstaller) still detect modules (#3232)

Assessment against linked issues: Out-of-scope changes

(No out-of-scope functional code changes detected; documentation additions and init behavior align with the linked issue objectives.)

"I nibble only when asked to dine,
NumPy waits — the world feels fine.
I hop on calls and load on cue,
Light-footed pygame, through and through.
A rabbit's wink — lazy, swift, and true. 🐇"

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
docs/reST/ref/sndarray.rst (1)

26-27: LGTM; consider adding a brief rationale to preempt “why avoid loading NumPy?”

The directive is correct. To address prior review feedback asking “why is it bad to load NumPy?”, add a short rationale to clarify the benefit.

 .. versionchanged:: 2.5.6 sndarray module is lazily loaded to avoid an expensive NumPy import when unnecessary
+   Rationale: reduces pygame import time and memory footprint for apps that don't use sndarray.
🧹 Nitpick comments (1)
src_py/__init__.py (1)

328-338: Optional: deduplicate the repeated optional-module initialization pattern.

Both blocks have the same structure (probe numpy -> check dependency -> MissingModule or lazy import). Consider factoring into a small helper to reduce duplication and keep future changes consistent.

Example helper (outside the shown ranges):

def _init_optional_lazy(name: str, deps: tuple[str, ...]) -> None:
    try:
        if numpy_missing:
            import numpy  # for MissingModule.reason
        for dep in deps:
            __import__("pygame." + dep)
    except (ImportError, OSError):
        globals()[name] = MissingModule(name, urgent=0)
    else:
        globals()[name] = lazy_import(name)

# Usage:
# _init_optional_lazy("surfarray", ("pixelcopy",))
# _init_optional_lazy("sndarray", ("mixer",))

Also applies to: 339-349

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ff6bbd and 7ae299a.

📒 Files selected for processing (3)
  • docs/reST/ref/sndarray.rst (1 hunks)
  • docs/reST/ref/surfarray.rst (1 hunks)
  • src_py/__init__.py (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src_py/__init__.py (1)
src_c/pixelcopy.c (1)
  • pixelcopy (1250-1275)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
src_py/__init__.py (3)

311-325: Hardened for missing spec/loader — matches prior feedback

The added guard for spec is None or spec.loader is None prevents AttributeError when modules are stripped. This addresses the earlier review concern.


349-357: Same pattern as surfarray; consider shared helper for maintainability

Mirrors the surfarray approach with pygame.mixer as the dependency. Consider the shared helper suggested above to reduce duplication and keep behavior in sync.


421-424: Make packager_imports resilient if numpy/deps are missing in packaging environments

Some tooling may execute or partially evaluate this function; unguarded imports of lazily-available modules can raise. Wrapping avoids hard failures while remaining visible to static scanners.

Apply:

-    # lazy imports
-    import pygame.surfarray
-    import pygame.sndarray
+    # lazy imports (guarded to avoid failures if executed by tooling)
+    try:
+        import pygame.surfarray
+    except Exception:
+        pass
+    try:
+        import pygame.sndarray
+    except Exception:
+        pass
🧹 Nitpick comments (2)
src_py/__init__.py (2)

311-331: LazyLoader pattern is correctly implemented; consider registering a stub for stripped builds (optional)

The LazyLoader flow matches the docs and uses the fully-qualified fullname key in sys.modules. Nice. For stripped builds (spec is None), you currently return a MissingModule but do not register it in sys.modules. If you want import pygame.surfarray to succeed with a stub in those builds (instead of raising ModuleNotFoundError), optionally register the stub:

Apply if you want import pygame. to resolve to the stub; otherwise, keep current behavior to continue raising on stripped builds.

 def lazy_import(name):
@@
-    if spec is None or spec.loader is None:
-        return MissingModule(name, urgent=0)
+    if spec is None or spec.loader is None:
+        m = MissingModule(name, urgent=0)
+        # Optionally register to let `import pygame.<name>` succeed with a stub
+        # Comment out the next line if you prefer a ModuleNotFoundError on stripped builds.
+        sys.modules[fullname] = m
+        return m

Please confirm the intended behavior for stripped builds:

  • Keep raising on import pygame.surfarray / pygame.sndarray (status quo)?
  • Or resolve to a MissingModule stub across all import forms?

338-347: Preflight dependency check + reason capture looks good; minor DRY opportunity

  • Intentionally importing numpy in the numpy_missing branch to capture an informative error message for MissingModule.reason is a pragmatic solution and avoids loading numpy otherwise.
  • Checking pygame.pixelcopy ensures surfarray won’t be lazily installed if a hard dependency is missing.

Optional: reduce duplication between surfarray/sndarray blocks by extracting a small helper.

Example helper (outside current ranges, for illustration):

def _maybe_lazy_import(submod, dep_module):
    try:
        if numpy_missing:
            import numpy  # force error for informative reason
        __import__(dep_module)
    except (ImportError, OSError):
        return MissingModule(submod, urgent=0)
    else:
        return lazy_import(submod)

# usage:
surfarray = _maybe_lazy_import("surfarray", "pygame.pixelcopy")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae299a and 0ef5f77.

📒 Files selected for processing (2)
  • docs/reST/ref/surfarray.rst (1 hunks)
  • src_py/__init__.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/reST/ref/surfarray.rst
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: arm64
  • GitHub Check: build (macos-14)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: aarch64
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: AMD64
  • GitHub Check: x86
  • GitHub Check: dev-check
🔇 Additional comments (4)
src_py/__init__.py (4)

76-83: Good: MissingModule now captures exception info for clearer errors

Capturing the exception type and message via sys.exc_info() and surfacing them in reason improves diagnostics and preserves prior semantics when constructed inside except blocks.


334-336: Lightweight NumPy availability check is appropriate

Using find_spec("numpy") avoids side effects and import-time cost. Good trade-off for the lazy-load gate.


359-359: Namespace cleanup is sensible

Deleting helper symbols keeps the pygame namespace tidy after initialization.


410-414: Clearer packager_imports docstring — good context for tooling

Calling out that the function is never executed and serves static scanners is helpful to avoid confusion.

Copy link
Member

@MyreMylar MyreMylar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, LGTM. I can verify the speedup in importing on my machine locally.

@MyreMylar MyreMylar merged commit 69114b4 into pygame-community:main Aug 25, 2025
28 checks passed
@aatle aatle deleted the lazy-numpy branch August 25, 2025 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Performance Related to the speed or resource usage of the project
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants