How Is Qtile Configured and Customized?
Updated
Qtile is configured entirely in Python: you write a config.py file that defines your key bindings, layouts, groups, screens, and window rules, and Qtile executes that file as code. That means any customization you can express in Python — loops, conditionals, functions, imports — is available to you, and you can reload the running window manager with lazy.reload_config() instead of restarting your session. This approach suits users comfortable reading and editing Python; if you prefer declarative config formats, Qtile's model will feel unusual.
The config file is a Python program
Qtile's own documentation describes it as "a full-featured, hackable tiling window manager written and configured in Python." The configuration is not a data file that Qtile parses — it is imported and run, so the structures you build (lists of keys, groups, layouts) are ordinary Python objects.
A typical config defines these pieces:
| Section | What it controls | Typical Python construct |
|---|---|---|
keys |
Key bindings and the actions they trigger | List of Key(...) objects |
layouts |
How windows are arranged on screen | List of layout instances |
groups |
Named workspaces and their layout assignment | List of Group(...) objects |
screens |
Which bars and widgets appear on which monitor | List of Screen(...) objects |
mouse |
Mouse button bindings | List of Drag/Click objects |
Because these are plain Python values, you can generate them programmatically. The default config itself does this for workspaces:
groups = [Group(i) for i in "123456789"]
for i in groups:
keys.extend([
Key([mod], i.name, lazy.group[i.name].toscreen(),
desc=f"Switch to group {i.name}"),
])
One list comprehension creates nine groups, and the loop attaches a switch-to-group binding for each one. Writing the same thing by hand would be nine near-identical blocks.
Key bindings: actions are lazy
Bindings pair a key combination with an action. Qtile wraps actions in lazy, which defers execution until the key is actually pressed — this is why you can reference qtile and its objects in a binding before the window manager has fully started.
From the default config:
Key([mod], "Return", lazy.spawn(terminal), desc="Launch terminal"),
Key([mod], "w", lazy.window.kill(), desc="Kill focused window"),
Key([mod], "f", lazy.window.toggle_fullscreen(),
desc="Toggle fullscreen on the focused window"),
Key([mod], "t", lazy.window.toggle_floating(),
desc="Toggle floating on the focused window"),
Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),
Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"),
The desc argument is not decoration — it feeds the command prompt and documentation, so a binding without one is harder to discover later.
Layout manipulation gets its own set of bindings, which is where tiling behavior becomes tunable at runtime:
Key([mod, "shift"], "k", lazy.layout.shuffle_up(), desc="Move window up"),
Key([mod, "control"], "h", lazy.layout.grow_left(), desc="Grow window to the left"),
Key([mod, "control"], "l", lazy.layout.grow_right(), desc="Grow window to the right"),
Key([mod], "n", lazy.layout.normalize(), desc="Reset all window sizes"),
Key([mod, "shift"], "Return", lazy.layout.toggle_split(),
desc="Toggle between split and unsplit sides of stack"),
The comment in the default config notes a useful edge case: if the focused window is already at the screen edge and you grow it in that direction, it shrinks instead. normalize() is the reset when your manual resizing has gotten away from you.
Conditional behavior with .when()
Not every binding makes sense on every backend. Qtile runs on both X11 and Wayland, and virtual-terminal switching only applies to Wayland. Rather than branching at config-load time — the config is loaded before Qtile starts, so qtile.core.name isn't available yet — the default config defers the check to keypress time:
for vt in range(1, 8):
keys.append(
Key(["control", "mod1"], f"f{vt}",
lazy.core.change_vt(vt).when(
func=lambda: qtile.core.name == "wayland"),
desc=f"Switch to VT {vt}"),
)
The .when(func=...) wrapper means the action only fires if the lambda returns true when the key is pressed. This pattern generalizes: any binding whose validity depends on runtime state can be guarded the same way.
Reloading without losing your session
lazy.reload_config() re-reads and re-executes your config file while Qtile keeps running. In practice this means you edit config.py, press your reload binding, and see the change — no logout, no restarting applications. This is the main reason the Python-as-config model stays workable despite being more verbose than a declarative format: the edit-test loop is short.
Two caveats worth knowing before you rely on it:
- A syntax error or exception in the config will surface at reload time, and Qtile's behavior on a failed reload depends on where the error occurs. Keep a known-good copy of your config before large edits.
- Reload re-runs the file, so any state you built up in Python objects (counters, caches) resets. If you need persistent state across reloads, store it outside the config.
Where customization actually pays off
The Python model is most valuable in three situations:
- Repetitive definitions. Workspaces, monitor setups, and per-application rules are naturally generated by loops, as the default config demonstrates with its nine groups.
- Logic that depends on state. Bindings or rules that should behave differently on Wayland vs. X11, or on a laptop vs. a docked monitor, can be expressed directly rather than through a config-format workaround.
- Integration with other Python code. Since the config is a Python module, it can import from your own modules or the standard library.
If none of those apply and you just want a static set of bindings, the Python requirement is overhead rather than an advantage — a declarative config would be shorter. The tradeoff is explicit: Qtile gives you a general-purpose language in exchange for writing more lines.
Getting started
The Qtile site links a Download page, Release Notes, and an Install Guide, and the current release listed is v0.37.0 (Aug 7, 2026). The default config shown throughout this article is the reference point to start from — copy it, then change one thing at a time and reload to confirm each change. Screenshots, videos, documentation, and the issue tracker are linked from the same site if you want to see configurations others have built before writing your own.