ModernGL needs OpenGL 3.3, not the modern OpenGL it advertises
Modern OpenGL binding for Python
At a glance
- What is it?
- ModernGL is a C++ implemented Python binding that collapses buffer setup into single calls. Its feature list claims to be faster than PyOpenGL while the notes section only claims it may be, and its own floor is OpenGL 3.3.
- Who is it for?
- ModernGL suits a Python developer writing a simulation, a small game or a custom renderer who wants buffers, textures and programs without the raw API ceremony. It does not suit a project that needs the full OpenGL feature set or arbitrary extensions, since the library says it implements neither.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 92 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three GL calls per buffer become one Python call
The argument for the library is made by counting lines. With PyOpenGL against the original API, binding a single vertex buffer takes three statements:
vbo1 = GL.glGenBuffers(1)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, vbo1)
GL.glBufferData(GL.GL_ARRAY_BUFFER, b"Hello World!", GL.GL_STATIC_DRAW)ModernGL turns each of those into one call per buffer, vbo1 = ctx.buffer(b"Hello World!") for a static allocation and vbo2 = ctx.buffer(reserve=1024, dynamic=True) when you want space reserved without filling it. The reserve and dynamic keywords are the difference that a raw call cannot express directly, since glBufferData with a null pointer and GL_DYNAMIC_DRAW is the conventional way to do it. The README puts the general case the same way: create a context with moderngl.get_context(), allocate with ctx.buffer(...), and read back with buf.read(), which returns the bytes you wrote. The stated reason for the speed is mechanical rather than clever: ModernGL is implemented in C++, and a single render call that would involve several OpenGL functions counts as one Python function call, so the interpreter overhead is paid once per call rather than once per GL entry point.
OpenGL 3.3 is the actual floor, 4.3 for compute shaders
The package describes itself as a modern OpenGL binding, and the requirement list is more precise than the slogan. ModernGL requires OpenGL 3.3. Compute shaders require OpenGL 4.3. Some functionality relies on specific extensions, which is the sentence to keep in mind when a feature is missing on a given driver. The stated scope is anywhere OpenGL is supported, and it explicitly includes headless environments, with an example file named headless_moderngl_logo.py and a separate headless directory of examples. The library also says plainly that it does not implement the full OpenGL feature set or extensions, and that you can interact with the ModernGL objects from OpenGL itself. That boundary is the honest shape of the design: it wraps the common path well and leaves the rest of the API reachable underneath.
Installing the core and the two extras that add capability
The core install is a single line:
pip install modernglTwo extras extend it, and they solve different problems. Headless context creation comes from the glcontext project, installed with pip install moderngl[headless], which is the path to take when there is no display and you still need a context. Window creation and resource loading come from a separate package, moderngl-window, installed with pip install moderngl-window, and the README calls it out as the way to integrate ModernGL with window libraries that support OpenGL, since the core library deliberately does not create windows itself. Development installs from a clone with python -m pip install -e ., after git clone and a cd into the directory. Packaging is a compiled extension: pyproject.toml names setuptools and wheel as the build requirements, and setup.py imports Extension, which is why a wheel rather than a pure-Python package is what lands in your environment.
Numpy and Pillow feed the GPU without a conversion layer
Two short examples define the interoperability story. With Pillow, you open an image, convert it to RGB, and hand the size, the channel count of 3 and the raw bytes straight to the context, ctx.texture(img.size, 3, img.tobytes()), which returns a Texture object. No intermediate array, no manual stride calculation, no byte-order handling. With Numpy it is the same idea on the buffer side, ctx.buffer(np.array([0.0, 0.0, 1.0, 1.0], dtype="f4")), which returns a Buffer. The dtype string matters, because the buffer format follows the array rather than something you declare separately. Those two integrations are the practical reason to pick this library over a raw binding: the data you already have in NumPy or a Pillow image reaches the GPU without a marshalling step. The repository also ships moderngl-stubs/, so type checkers get signatures for the API rather than treating every call as dynamic.
Fifteen numbered examples form the real tutorial
The examples directory is the documentation that matters, and it is structured as an ordered course. The files run from 01_hello_world.py through 02_shader.py, 03_camera.py, 04_vertex_buffer.py, 05_uniforms.py, 06_multiple_objects.py, 07_different_geometries.py, 08_texture.py, 09_models_and_images.py, 10_lighting.py, 11_multiple_programs.py and 12_uniform_buffer.py, then 13_shader_includes.py, 14_postprocessing.py and 15_pillow_overlay.py. After the numbered sequence there are standalone files for the cases that do not fit a lesson: external_buffer.py and external_texture.py for sharing data with other libraries, imgui_integration.py for a GUI toolkit, layered_rendering.py, mesh_shaders.py, moderngl_window_empty.py and moderngl_window_fractal.py, plus a headless logo example and a data directory. That ordering is a better guide to the API surface than the feature list, because each file adds one concept in the order a newcomer needs it.
The README advertises speed, then hedges it
There is a small internal inconsistency worth naming, because it tells you how much weight to put on the performance claim. The feature list states ModernGL is simpler and faster than PyOpenGL as a flat assertion. Further down, the notes section says ModernGL may be faster than other libraries providing direct OpenGL access, which is a different claim, hedged, and about a broader set of libraries than the one named above. Neither statement includes a measurement, a version comparison, or a workload. The citation block compounds it: the reference title describes the project as high performance python bindings for OpenGL 3.3+, which is a bibliographic description rather than a result. On maintenance, the published tags are 5.12.0 from 2024-10-17, 5.11.1 and 5.11.0 both from 2024-08-13, while the last push to the default branch was on 2026-07-11, so the branch is well ahead of the newest tag and a pip install gives you whatever the index currently holds.
Where ModernGL is the wrong tool
The first wrong case is a project that needs a specific extension. The library says some functionality relies on specific extensions and that it does not implement the full OpenGL feature set, so a renderer built around compute-heavy or vendor-specific work has to drop into the raw API for those parts, which means two APIs in one codebase. The second is a driver floor. OpenGL 3.3 is the requirement and 4.3 for compute shaders, so on a headless server with a software rasterizer or an old virtual GPU, the context creation is where you find out, and the answer is not something the library can fix. The third is expecting the library to own your window. It deliberately does not create windows, which is a strength when you already have a GLFW, SDL or Qt window, and a limitation when you wanted one binding that did everything; moderngl-window is a separate package for that. The fourth is a CPU-only data pipeline. This is a GPU binding, so if the transformation you need is a reshape or a reduction over an array, NumPy is the right tool and ModernGL is the wrong one, no matter how clean the ctx.buffer call looks.
Editorial conclusion
ModernGL suits a Python developer writing a simulation, a small game or a custom renderer who wants buffers, textures and programs without the raw API ceremony. It does not suit a project that needs the full OpenGL feature set or arbitrary extensions, since the library says it implements neither. Before adopting, install with pip install moderngl, confirm the host reports OpenGL 3.3 at minimum, and reach for moderngl[headless] or moderngl-window only when you need those paths.
Frequently asked questions
What OpenGL version does ModernGL require?
ModernGL requires OpenGL 3.3, and compute shaders require OpenGL 4.3. Some functionality depends on specific extensions, and the library does not implement the full OpenGL feature set.
How do I install ModernGL for headless rendering?
Install the core with pip install moderngl, then add the headless extra with pip install moderngl[headless], which pulls in the glcontext project for headless context creation.
How does ModernGL compare to PyOpenGL for buffer setup?
PyOpenGL needs glGenBuffers, glBindBuffer and glBufferData per buffer. ModernGL replaces that with one call each, ctx.buffer(b"Hello World!") for static data and ctx.buffer(reserve=1024, dynamic=True) for reserved space.
Does ModernGL create windows for me?
No. Window creation and resource loading live in the separate moderngl-window package, which implements several window libraries. ModernGL integrates with any window library that supports OpenGL.
Can ModernGL take data directly from Numpy or Pillow?
Yes. ctx.buffer(np.array([0.0, 0.0, 1.0, 1.0], dtype="f4")) accepts a Numpy array, and ctx.texture(img.size, 3, img.tobytes()) takes a converted Pillow image with its size and channel count.