2026, Jan 10 09:00

How to Expose Submodules in a Python Package (__init__.py) to Avoid AttributeError in TUI Toolkits

Learn why Python packages raise AttributeError: module has no attribute when accessing submodules, and fix it by exposing them in __init__.py for a TUI API.

When building a Python package for a TUI, it’s easy to run into an AttributeError right after a seemingly correct import. A common pattern looks like a package that works if you import a submodule directly, but fails when you try to access that submodule through the package namespace. The root cause is almost always the same: the package’s public API isn’t exposing the submodule.

Repro: what fails and what works

Here’s a minimal example that triggers the error when addressing a submodule via the top-level package object:

import toolkit as tk
tk.render.frame('+', None, None, 50, 25)

In this setup, Python raises an AttributeError similar to:

AttributeError: module 'toolkit' has no attribute 'render'

However, importing the submodule directly works as expected:

from toolkit import render as rnd
rnd.frame('+', None, None, 50, 25)

The drawing routine itself is straightforward; for context, here’s an example implementation of the callable inside the submodule:

from ..styling import rgb, color_nameX, color_name, color_like
from sys import stdout as stream
def frame(glyph: str, fg: color_like | None, bg: color_like | None, w: int, hgt: int) -> None:
    if fg:
        fgx = fg
        fgx.graund = "fore"
    else:
        fgx = False
    if bg:
        bgx = bg
        bgx.graund = "back"
    else:
        bgx = False
    for row in range(hgt):
        if fgx:
            stream.write(str(fgx))
        if bgx:
            stream.write(str(bgx))
        stream.write(glyph[0] * w)
        if fgx:
            stream.write("\x1b[39m")
        if bgx:
            stream.write("\x1b[49m")
        stream.write("\n")

Why this happens

The package-level import returns a package object defined by its top-level __init__.py. If that __init__.py doesn’t explicitly expose the submodule, attribute access like package.submodule won’t be available. By contrast, importing the submodule directly binds it by name and succeeds on its own.

The fix

Expose the submodule in the top-level package and re-export the callable you want to use. Place your test script at the same level as the package folder so the import resolves the local package, then wire up the public API.

toolkit/__init__.py

from . import render

toolkit/render/__init__.py

from .forms import frame

With these changes, the original usage works as intended:

import toolkit as tk
tk.render.frame('+', None, None, 50, 25)

Why it’s worth knowing

Top-level __init__.py defines what your package presents to users. If a submodule isn’t imported or a symbol isn’t re-exported, accessing it via package.attribute will fail, even though direct imports might appear fine. Being deliberate about what you expose avoids confusing AttributeError surprises and makes your package API feel consistent.

Takeaways

When you hit “module has no attribute submodule” in a package context, check the public surface. Ensure the top-level __init__.py imports the submodule you expect to be reachable, and that the submodule’s own __init__.py re-exports the names you want to be public. Keep your test script alongside the package during development so Python resolves the intended local package. A clean, explicit export path saves time and keeps your TUI toolkit ergonomic to use.