#6339·nicegui

`Layer.current_leaflet` keeps the last `ui.leaflet` element alive after its client is gone

Author: falkoschindlerCreated Sep 11, 2026Updated Sep 11, 2026
Labelsbug

Description

Layer.current_leaflet is a ClassVar on Layer (leaflet_layer.py:16) that is written in two places and cleared in none:

  • Leaflet.__enter__ (leaflet.py:88), and
  • Leaflet.__getattribute__ (leaflet.py:91-95), which sets it whenever an attribute that is a Layer subclass is read — this is how m.marker(...) tells the new Marker which map it belongs to, since Layer.__post_init__ reads the class variable rather than an argument.

So after the last ui.leaflet on a page has been used, the class variable still points at it, and the reference outlives the page: deleting the client removes the element from client.elements, but the ClassVar keeps the element object itself — and its layers list, plus anything those layers close over — reachable until the next ui.leaflet anywhere in the process replaces it.

Measured with a User test, with a ui.label on the same page as the control:

python
import gc
import weakref

from nicegui import ui
from nicegui.testing import User


async def test_leaflet_survives_client_deletion(user: User) -> None:
    refs = {}

    @ui.page('/')
    def page():
        refs['label'] = weakref.ref(ui.label('hello'))
        refs['leaflet'] = weakref.ref(ui.leaflet(center=(51.5, -0.09)))

    await user.open('/')
    user.client.delete()
    gc.collect()

    assert refs['label']() is None      # collected
    assert refs['leaflet']() is None    # fails: still alive

The label is collected, the map is not. Element._client is a weakref, so the client itself is not held — what leaks is the element plus its layer graph, per process, indefinitely.

Beyond the leak

The same class variable is also the only channel that binds a layer to its map, which makes the binding depend on global state rather than on the call that creates the layer. A __getattribute__ override that writes global state on attribute access is a lot of machinery for passing one argument, and AGENTS.md rules out global mutable state in library code.

So there are two levels of fix:

  1. Small: clear Layer.current_leaflet in Leaflet._handle_delete when it points at the element being deleted. Closes the leak, keeps the mechanism.
  2. Proper: pass the map explicitly when constructing a layer and drop both the ClassVar and the __getattribute__ override. That changes how layer classes are reached, so it wants 4.0.

Found while reviewing #6294, where it is out of scope.