Python Dear PyGui — Deep Dive
Architecture overview
Dear PyGui (DPG) operates in three layers:
- Python API — the
dearpygui.dearpyguimodule you call from Python. - C++ core — wraps Dear ImGui, ImPlot, and ImNodes into a retained-mode item system.
- GPU backend — renders through DirectX 11/12 (Windows), Metal (macOS), or OpenGL/Vulkan (Linux).
Each frame, the C++ core traverses the item tree, generates ImGui draw lists, and submits them to the GPU. Python callbacks fire between frames on the main thread.
Custom render loop
For applications that need per-frame logic (simulations, live data feeds), replace start_dearpygui() with a manual loop:
dpg.create_context()
dpg.create_viewport(title="Simulation", width=1024, height=768)
dpg.setup_dearpygui()
dpg.show_viewport()
frame = 0
while dpg.is_dearpygui_running():
# Update simulation state
frame += 1
positions = simulate_step(frame)
dpg.set_value("plot_series", [list(range(len(positions))), positions])
dpg.render_dearpygui_frame()
dpg.destroy_context()
This gives you full control over the update loop while DPG handles rendering.
Dynamic layout patterns
Conditional visibility
dpg.add_checkbox(label="Advanced Settings", callback=toggle_advanced)
advanced_group = dpg.add_group(show=False, tag="advanced")
def toggle_advanced(sender, value):
dpg.configure_item("advanced", show=value)
Runtime item creation
def add_row(sender, app_data):
with dpg.table_row(parent="data_table"):
dpg.add_input_text(default_value="New Item")
dpg.add_input_float(default_value=0.0)
dpg.add_button(label="X", callback=lambda s: dpg.delete_item(
dpg.get_item_parent(s)))
Items can be created, moved (dpg.move_item), and deleted at any time. The item tree updates on the next frame.
Texture loading and image display
DPG can display images via textures:
from PIL import Image
import numpy as np
img = Image.open("photo.png").convert("RGBA")
data = np.array(img).flatten() / 255.0 # normalize to 0-1
with dpg.texture_registry():
dpg.add_static_texture(
width=img.width, height=img.height,
default_value=data.tolist(),
tag="photo_tex"
)
with dpg.window(label="Viewer"):
dpg.add_image("photo_tex")
For video or streaming data, use dpg.add_dynamic_texture() and update pixel data each frame with dpg.set_value().
Node editor for visual programming
DPG includes a node editor (wrapping ImNodes) for building visual graphs:
with dpg.window(label="Pipeline Editor"):
with dpg.node_editor(callback=link_callback,
delink_callback=delink_callback,
tag="node_editor"):
with dpg.node(label="Input", tag="node_input", pos=(20, 50)):
with dpg.node_attribute(attribute_type=dpg.mvNode_Attr_Output,
tag="input_out"):
dpg.add_input_text(label="File", width=150)
with dpg.node(label="Process", tag="node_process", pos=(250, 50)):
with dpg.node_attribute(attribute_type=dpg.mvNode_Attr_Input,
tag="process_in"):
dpg.add_text("Input")
with dpg.node_attribute(attribute_type=dpg.mvNode_Attr_Output,
tag="process_out"):
dpg.add_combo(["Filter", "Transform", "Aggregate"],
default_value="Filter", width=120)
def link_callback(sender, app_data):
dpg.add_node_link(app_data[0], app_data[1], parent=sender)
def delink_callback(sender, app_data):
dpg.delete_item(app_data)
This is useful for ML pipeline builders, shader editors, audio routing, and data flow visualization.
File dialogs and OS integration
with dpg.file_dialog(directory_selector=False, show=False,
callback=file_selected, tag="file_dlg",
width=700, height=400):
dpg.add_file_extension(".csv", color=(0, 255, 0))
dpg.add_file_extension(".json", color=(0, 0, 255))
dpg.add_button(label="Open File",
callback=lambda: dpg.show_item("file_dlg"))
DPG renders its own file dialog (not OS-native). For native dialogs, pair with tkinter.filedialog or the plyer library.
Multi-viewport and docking
DPG supports Dear ImGui’s docking branch:
dpg.configure_app(docking=True, docking_space=True)
With docking enabled, users can drag windows to dock them side-by-side, stack as tabs, or float independently — similar to IDE layouts. Each window becomes a dockable panel.
Performance profiling
DPG includes a built-in metrics window:
dpg.show_metrics() # opens the Dear ImGui metrics/debug window
This shows:
- Frame rate and frame time breakdown
- Number of draw calls and vertices
- Item count and allocation stats
- Active windows and their rendering cost
Optimization strategies
- Reduce item count — DPG traverses every item each frame. Use
show=Falseon off-screen panels. - Batch plot updates — update series data in one
set_value()call instead of appending point-by-point. - Clip large tables — DPG tables auto-clip rows, but minimize column count for very wide tables.
- Avoid Python callbacks on hot paths — if a slider callback triggers heavy computation, debounce it:
import time
_last_call = 0
def debounced_callback(sender, value):
global _last_call
now = time.time()
if now - _last_call > 0.1: # max 10 updates/sec
_last_call = now
expensive_update(value)
- Use dynamic textures wisely — updating a 1920x1080 texture at 60fps pushes ~500MB/s through the CPU-GPU bus. Resize to the display size first.
Integration with data science stack
DPG works well alongside NumPy, Pandas, and ML frameworks:
import numpy as np
# Live training dashboard
epochs = []
losses = []
def on_epoch_end(epoch, loss):
epochs.append(epoch)
losses.append(loss)
dpg.set_value("loss_series", [epochs, losses])
dpg.set_value("epoch_text", f"Epoch: {epoch}")
dpg.set_value("loss_text", f"Loss: {loss:.4f}")
The GPU rendering means charts update without frame drops even while the main thread is also running model training (via a background thread feeding data through a queue).
Packaging
DPG bundles its dependencies as compiled extensions. PyInstaller works out of the box:
pyinstaller --windowed main.py
The resulting binary is self-contained — no Tcl/Tk or Qt runtime needed. Binary size is typically 15-25 MB.
State management for complex apps
As DPG applications grow, centralize state instead of scattering get_value/set_value calls:
from dataclasses import dataclass, field
from typing import Any
@dataclass
class AppState:
learning_rate: float = 0.01
epochs: int = 100
losses: list = field(default_factory=list)
running: bool = False
def update(self, key: str, value: Any):
setattr(self, key, value)
self._sync_ui(key, value)
def _sync_ui(self, key: str, value: Any):
tag = f"ui_{key}"
if dpg.does_item_exist(tag):
dpg.set_value(tag, value)
state = AppState()
# Bind UI to state
dpg.add_slider_float(label="LR", tag="ui_learning_rate",
default_value=state.learning_rate,
callback=lambda s, v: state.update("learning_rate", v))
This pattern keeps the UI and business logic loosely coupled, making testing possible without rendering frames.
One thing to remember: Dear PyGui’s sweet spot is real-time tool UIs — the custom render loop, node editor, and 60fps plotting make it unmatched for dashboards and parameter tuning interfaces. For document-heavy or OS-native apps, pair it with (or replace it by) a traditional framework.
See Also
- Python Kivy Mobile Apps Imagine writing one recipe that works in every kitchen — Kivy lets you build a single Python app that runs on phones, tablets, and computers.
- Python Pyqt Desktop Apps Imagine a professional LEGO set for building real desktop apps — that's PyQt, giving Python the same powerful toolkit used by VLC, Dropbox, and Calibre.
- Python Tkinter Gui Think of building a cardboard control panel to understand how Python's built-in Tkinter lets you create windows, buttons, and text boxes without installing anything extra.
- Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.
- Containerization Why does software that works on your computer break on everyone else's? Containers fix that — and they're why Netflix can deploy 100 updates a day without the site going down.