This chapter teaches the basic Python you need before reading the intermediate guide.
The goal is not to memorize every method in the codebase. The goal is to understand the syntax patterns that appear again and again in LifeXP.
Read this guide alongside the modular LifeXP files:
- main.py: The clean, modular application entry point.
- lifexp/constants.py: Holds balance constants, version numbers, font sizing, and visual timings.
- lifexp/runtime.py: Handles OS, file paths, packaging detection, and platform scaling.
- lifexp/ui_mixin.py: Builds all visual tabs, quest logs, active quest items, and theme styles.
- lifexp/data_mixin.py: Manages saving progress to JSON, loading profiles, backups, and resets.
- lifexp/engine_mixin.py: Controls core RPG calculations, XP gains, level curves, and task completions.
- lifexp/animation_mixin.py: Drives high-fidelity canvas animations, particle explosions, and floating text.
For each Python idea, study it in this order:
- Learn what the syntax means.
- Read the real example from LifeXP.
- Follow what the computer reads, line by line.
- Look at the small diagram.
- Find one more similar example in the project.
When code feels confusing, slow down and ask:
- What value exists right now?
- What line runs next?
- Did an
ifskip anything? - Did a loop repeat?
- Did a function return a value?
- Did a Tkinter callback wait for a user click?
LifeXP is a Tkinter desktop app. The user clicks buttons, the app changes Python data, and then the app redraws widgets.
An import brings code from another module into the current file.
LifeXP uses imports for:
- Tkinter windows and widgets
- JSON save files
- Dates, time, and math functions
- Constants from
lifexp/constants.py - Helper functions from
lifexp/runtime.py - Modular Mixin classes that group related behaviors into separate files
In main.py:
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from lifexp.constants import BASE_XP_NEEDED, DEFAULT_FONT_SIZE
from lifexp.runtime import get_resource_dir, get_user_data_dir, is_packaged_app
from lifexp.ui_mixin import UIMixin
from lifexp.data_mixin import DataMixin
from lifexp.engine_mixin import EngineMixin
from lifexp.animation_mixin import AnimationMixin- Load standard GUI modules like
tkinter,ttk, andmessagebox. - Load global balancing settings like
BASE_XP_NEEDEDfrom the constants file. - Load system environment helpers from
runtime.py. - Load our custom modular Mixins (
UIMixin,DataMixin,EngineMixin,AnimationMixin) so we can assemble the final app from separate specialized files.
A variable stores a value under a name.
Python reads assignment from right to left:
name = valueMeans:
- Create or calculate the value on the right.
- Store it in the name on the left.
self.current_theme_name = "Tokyo Night"
self.font_size = DEFAULT_FONT_SIZE
self.animations_enabled = True- Store the text
"Tokyo Night"as the current theme name. - Store the default font size from the constants file.
- Store
True, meaning animations are enabled.
These names are attached to self, so they belong to the current LifeXP app object.
A constant is a normal variable that the project treats as a fixed setting. In modern modular projects, we keep these in a dedicated file so they are easy to find and adjust.
Python does not force constants to stay unchanged. The uppercase naming is a signal to humans: "This is a setting. Do not casually rewrite it."
In lifexp/constants.py:
BASE_XP_NEEDED = 100
ACCOUNT_BASE_XP_NEEDED = 500
XP_POPUP_STEPS = 125- Remember that basic attribute levels start from
100XP. - Remember that account levels start from
500XP. - Remember that the XP popup animation lasts
125steps. - Any other file in the app can import and read these settings without needing to load UI or animation logic.
Python values have types.
The beginner types you see constantly in LifeXP are:
str: text, like"Strength"int: whole numbers, like100bool:TrueorFalseNone: no value
self.current_theme_name = "Tokyo Night"
self.current_total_level = 0
self.animations_enabled = True
self.tab_selected_bg = None"Tokyo Night"is text.0is a number.Trueis an on/off value.Nonemeans no selected tab background has been stored yet.
A list stores ordered items.
Lists use square brackets:
items = ["first", "second", "third"]You use lists when order matters, when you may have many items, or when you want to loop through items.
self.attributes = ["Strength", "Agility", "Intelligence", "Charisma", "Vitality"]Another example:
level_events = []
level_events.extend(self.gain_xp(attr, xp_gain))For self.attributes:
- Create a list with five strings.
- Store the list on the app object.
- Later, loops use this list to build stats, colors, progress bars, trophies, and reports.
For level_events:
- Start with an empty list.
- Add new level-up events returned by
gain_xp. - Use the list later for animations.
A dictionary stores values by name.
Dictionaries use curly braces:
person = {
"name": "Hero",
"level": 3
}You ask for a value with its key:
person["level"]normalized.append({
"name": name,
"attribute": attr,
"subcategory": str(task.get("subcategory") or name).strip() or name,
"xp": xp
})- Build one dictionary for one quest.
- Store the quest name under
"name". - Store the scaling attribute under
"attribute". - Store the activity category under
"subcategory". - Store the XP reward under
"xp". - Append that dictionary to the
normalizedlist.
Real apps often combine lists and dictionaries.
LifeXP's save data is nested:
self.datais a dictionary.self.data["tasks"]is a list.- Each task inside that list is a dictionary.
self.data["stats"]is a dictionary.- Each stat inside that dictionary is another dictionary.
stat = self.data["stats"][attribute]
stat["xp"] += amount- Go into
self.data. - Find the
"stats"dictionary. - Inside
"stats", find the currentattribute, such as"Strength". - Store that stat dictionary in
stat. - Add the reward amount to
stat["xp"].
def creates a function.
Inside a class, a function is usually called a method.
The method body is the indented code under the def line.
def get_xp_needed(self, level):
"""Returns the XP required to pass the given attribute level."""
if level not in self.xp_needed_cache:
self.xp_needed_cache[level] = self.get_scaled_xp_needed(level, BASE_XP_NEEDED)
return self.xp_needed_cache[level]When Python first sees this code, it does not run the method body. It stores the method definition.
Later, when code calls self.get_xp_needed(level), Python does this:
- Put the app object into
self. - Put the given number into
level. - Check whether the level is already cached.
- If not cached, calculate and store it.
- Return the XP cost.
A class groups related data and behavior.
To keep a large application clean, we use a modular design pattern called Mixins combined with Multiple Inheritance. Instead of writing thousands of lines in one giant class file, we split the code into separate mixin classes:
UIMixinmanages visual layouts.DataMixinmanages JSON file persistence.EngineMixinmanages RPG rules and math.AnimationMixinmanages particles and fireworks.
We then inherit from all of them in our main LifeXPApp class. self means "this exact instantiated app object." Even though methods are written in different files, self connects them all! When a method in EngineMixin calls self.save_data(), Python looks across all mixins and finds save_data in DataMixin.
In main.py:
class LifeXPApp(UIMixin, DataMixin, EngineMixin, AnimationMixin):
def __init__(self, root):
self.root = root
self.attributes = ["Strength", "Agility", "Intelligence", "Charisma", "Vitality"]
# ...
self.data = self.load_data() # load_data is defined in DataMixin!- Create a class
LifeXPAppthat inherits all methods and variables from our four mixin files. - When
LifeXPAppstarts, run__init__to store core variables. - Store the Tkinter root window in
self.root. - Define the
self.attributeslist, which will be accessible by all mixins. - Call
self.load_data(). Because of mixins, the computer finds this method insideDataMixin, readslifexp_data.json, and attaches the result directly toself.data.
Python uses indentation to decide what code belongs together.
This is not decoration. Indentation changes meaning.
if not indices:
return
tasks = [self.data["tasks"][index] for index in indices]- Check
if not indices. - If that condition is true, run the indented
return. - The
tasks = ...line is not indented under theif, so it runs only if the method did not already return.
An if statement chooses whether a block of code runs.
An early return stops the current method immediately.
indices = self.get_selected_task_indices("Select a quest to complete it.")
if not indices:
return- Ask the UI which quest rows are selected.
- Store the selected indexes in
indices. - If the list is empty, stop the method.
- If the list has indexes, keep going.
This prevents the app from trying to complete a quest when the user selected nothing.
A for loop repeats once for each item in a collection.
Basic shape:
for item in items:
do_something(item)LifeXP uses for loops when it needs to process every task, every stat, every widget, every theme color, or every selected row.
for task in tasks:
attr = task["attribute"]
xp_gain = task["xp"]
total_xp_gain += xp_gain
level_events.extend(self.gain_xp(attr, xp_gain))Imagine tasks contains three completed quests.
- Take the first task dictionary.
- Store it in the variable
task. - Read its
"attribute". - Read its
"xp". - Add that XP to the total.
- Give XP to the correct attribute.
- Go back to the top of the loop for the second task.
- Repeat for the third task.
- When no tasks remain, leave the loop.
A while loop repeats while a condition stays true.
Basic shape:
while condition:
do_work()Use a while loop when you do not know ahead of time how many repeats are needed.
xp_needed = self.get_xp_needed(stat["level"])
while stat["xp"] >= xp_needed:
stat["xp"] -= xp_needed
stat["level"] += 1
xp_needed = self.get_xp_needed(stat["level"])Imagine a quest gives a lot of XP.
- Calculate how much XP is needed for the current level.
- Ask: does the stat have enough XP?
- If no, skip the loop.
- If yes, subtract the level cost.
- Increase the level by one.
- Calculate the new level cost.
- Ask the condition again.
- Repeat until the remaining XP is lower than the current level cost.
This is why one large quest can cause multiple level-ups.
A list comprehension is a compact way to build a new list.
Basic shape:
new_list = [make_item(old_item) for old_item in old_list]It is still a loop. It is just written inside square brackets.
tasks = [self.data["tasks"][index] for index in indices]- Start a new list.
- Take the first selected
index. - Read
self.data["tasks"][index]. - Put that task dictionary into the new list.
- Repeat for each selected index.
- Store the finished list in
tasks.
Longer version:
tasks = []
for index in indices:
tasks.append(self.data["tasks"][index])enumerate gives you both the index and the item while looping.
Basic shape:
for index, item in enumerate(items):
...for i, task in enumerate(self.data["tasks"]):
self.task_tree.insert("", tk.END, iid=i, values=(task["name"], task["attribute"], f"{task['xp']} XP"))- Look at
self.data["tasks"]. - For the first task, set
ito0andtaskto the first task dictionary. - Insert one visual row into the Tkinter table.
- Use
iid=iso the visual row knows which list index it represents. - Repeat for the next task with
iset to1.
Objects have methods. A method call looks like this:
object.method(arguments)LifeXP uses these list and dictionary methods constantly.
self.data["history"].append({
"name": task["name"],
"attribute": attr,
"subcategory": task.get("subcategory", "General"),
"xp": xp_gain,
"date": datetime.now().isoformat()
})level_events.extend(self.gain_xp(attr, xp_gain))self.data["tasks"].pop(index)task.get("subcategory", "General").append(...): add one item to the end of a list..extend(...): add many items from another list..pop(index): remove and return one item at that index..get(key, fallback): read a dictionary key, or use the fallback if the key is missing.
try and except let the app attempt risky code without crashing.
Use this when something might fail:
- reading a damaged JSON file
- converting text to a number
- working with OS files
- asking Tkinter for a window property that may not exist yet
try:
xp = max(1, int(task.get("xp", 0)))
except (TypeError, ValueError):
continue- Try to read the task XP.
- Try to convert it to an integer.
- Force it to be at least
1. - If the value cannot be converted, jump to
except. continueskips this bad task and moves to the next task in the loop.
with open(...) as f: opens a file and automatically closes it afterward.
JSON is a text format that can store Python-like dictionaries and lists.
LifeXP uses JSON as the save file.
with open(self.data_file, 'r', encoding="utf-8") as f:
data = json.load(f)Saving goes the other direction:
with open(temp_file, 'w', encoding="utf-8") as f:
json.dump(self.data, f, indent=4)For loading:
- Open the save file for reading.
- Call the opened file
f. - Convert the JSON text into Python data.
- Store that Python data in
data. - Close the file automatically.
For saving:
- Open the temporary save file for writing.
- Convert
self.datainto JSON text. - Write it to the file.
- Close the file automatically.
An f-string lets you put variables inside text.
Basic shape:
message = f"Hello {name}"popup_text = f"+{total_xp_gain} XP!" if len(tasks) == 1 else f"{len(tasks)} QUESTS +{total_xp_gain} XP!"Another example:
f"{task['xp']} XP"For f"{task['xp']} XP":
- Read the task's XP number.
- Convert it into text.
- Put it before
" XP".
If the task has 25 XP, the final text is:
"25 XP"Comparisons ask true-or-false questions.
Common examples:
==: equal!=: not equal<: less than<=: less than or equal>: greater than>=: greater than or equalin: exists inside a collectionnot: reverses true/falseand: both conditions must be trueor: at least one condition must be true
if not 0 <= index < len(self.data["tasks"]):
self.refresh_task_list()
return []- Check whether
indexis at least0. - Check whether
indexis smaller than the number of tasks. - If that full range check is false,
notturns it into true. - The app refreshes the task list and returns an empty list.
This protects the app from using an invalid row index.
Tkinter is Python's built-in desktop UI toolkit.
Important Tkinter ideas in LifeXP:
- A
TkorToplevelis a window. - A
Frameis a container. - A
Labelshows text. - A
Buttonwaits for clicks. - A
Treeviewshows table rows. .pack(...)and.grid(...)place widgets on the screen..bind(...)connects keyboard or mouse events to methods.command=...connects a button click to a method.
page = tk.Frame(self.tab_tasks, bg=self.bg_dark)
page.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
list_frame = tk.Frame(page, bg=self.bg_light)
list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 14), pady=0)
tk.Label(
list_frame,
text="Active Quests",
bg=self.bg_light,
fg=self.text_color,
font=("{San Francisco}", 16, "bold")
).pack(anchor=tk.W, padx=16, pady=(14, 10))Button callback example:
self.create_quest_action_button(
action_stack,
"+",
"Accept Quest",
"accept",
self.add_task_dialog
).pack(fill=tk.X, pady=(0, 9))For the frame and label:
- Create a frame inside the task tab.
- Store it in
page. - Pack it so it fills available space.
- Create a second frame inside
page. - Store it in
list_frame. - Create a label inside
list_frame. - Set the label text, colors, and font.
- Pack the label so it appears on screen.
For the button:
- Create a quest action button.
- Pass
self.add_task_dialogas the command. - Do not run
add_task_dialogimmediately. - Wait until the user clicks the button.
- Then Tkinter calls
self.add_task_dialog.
An event callback is a function that runs later because something happened.
In normal code, Python runs top to bottom immediately.
In GUI code, Python builds the interface first. Then it waits for events like clicks, keypresses, and scrolls.
self.task_tree.bind("<Command-Button-1>", self.toggle_task_tree_selection)
self.task_tree.bind("<Control-Button-1>", self.toggle_task_tree_selection)- Tell the task table to watch for Command-click.
- Tell the task table to watch for Control-click.
- Do not call
toggle_task_tree_selectionright now. - Later, when the event happens, Tkinter calls the method and passes an event object.
return sends a result back to the caller and ends the current function.
Some methods return useful values. Some methods mainly change app state and return nothing.
def get_total_xp_for_stat(self, stat):
"""Returns lifetime XP for one attribute, including already-spent level XP."""
return stat["xp"] + self.get_total_xp_before_level(stat["level"])- Read the XP inside the current level.
- Calculate XP spent before this level.
- Add them together.
- Return the total to whoever called this method.
Now combine the basics. See how different modular files work together through the single self object!
In lifexp/engine_mixin.py:
def complete_task(self):
"""The main gameplay loop: Finish task -> Grant XP -> Save History -> Redraw."""
indices = self.get_selected_task_indices("Select a quest to complete it.")
if not indices:
return
tasks = [self.data["tasks"][index] for index in indices]
level_events = []
total_xp_gain = 0
for task in tasks:
attr = task["attribute"]
xp_gain = task["xp"]
total_xp_gain += xp_gain
level_events.extend(self.gain_xp(attr, xp_gain))
self.data["history"].append({
"name": task["name"],
"attribute": attr,
"subcategory": task.get("subcategory", "General"),
"xp": xp_gain,
"date": datetime.now().isoformat()
})
for index in sorted(indices, reverse=True):
self.data["tasks"].pop(index)
self.save_data()
self.refresh_task_list()
rank_event = self.update_stats_display(animate_rank=False)
level_events = self.summarize_level_events(level_events)
cx, cy = self.get_center()
popup_text = f"+{total_xp_gain} XP!" if len(tasks) == 1 else f"{len(tasks)} QUESTS +{total_xp_gain} XP!"
popup_box = self.play_floating_text(popup_text, "#EBCB8B", cx, cy, size=30, duration_steps=XP_POPUP_STEPS, fade_steps=XP_POPUP_FADE_STEPS)
self.play_firework_particles("#EBCB8B", popup_box, count=34, palette=["#EBCB8B", "#FFD166", "#F59E0B", "#F97316"], physics=True, life_range=(42, 58), fade_start_ratio=0.42)
if level_events or rank_event:
self.schedule_level_up_sequence(level_events, rank_event)get_selected_task_indices(): Query the Tkinter Treeview table (fromui_mixin.py) to find selected quest row indexes.- Early
return: If the user clicked complete without selecting a quest, exit immediately. - List Comprehension: Read each selected task dictionary from our loaded profile tasks (
self.data["tasks"]). forLoop: Iterate over selected tasks to extract attributes and XP, callself.gain_xp(), and append completed records toself.data["history"]with ISO formatted timestamps.- Reverse Sorted Loop: Safely delete the completed quests from
self.data["tasks"](iterating backwards to avoid indexing shift bugs). self.save_data(): Save the modified profile dictionaries back into the JSON file (defined indata_mixin.py).self.refresh_task_list()&self.update_stats_display(): Re-render the tables and progress bars to display the new levels and XP values (defined inui_mixin.py).self.play_floating_text()&self.play_firework_particles(): Spawn premium visual effects on the canvas, drawing glowing XP popups and launching custom fireworks from the popups (defined inanimation_mixin.py).
Use this checklist when reading any LifeXP method:
- Find the method name after
def. - Identify the inputs in parentheses.
- Notice which variables are created.
- Track every
self.somethingbecause it belongs to the app object. - For every
if, ask which branch runs. - For every
for, ask what collection is being looped through. - For every
while, ask what condition eventually becomes false. - For every dictionary, identify the keys.
- For every list, ask what kind of items it stores.
- For every Tkinter widget, ask where it is placed and what callback it uses.
- For every
return, ask what value goes back to the caller.
Do these before moving to the intermediate guide.
Find this line in main.py:
self.attributes = ["Strength", "Agility", "Intelligence", "Charisma", "Vitality"]Then search across the mixin files (e.g. in ui_mixin.py or engine_mixin.py) for:
for attr in self.attributes:Write down what each loop does with each attribute.
Find where a task dictionary is appended in normalize_tasks (in data_mixin.py) or add_task_dialog (in ui_mixin.py).
Answer:
- What keys does one task have?
- Which method displays those keys in the table (see
refresh_task_listinui_mixin.py)? - Which method removes the task after completion (see
complete_taskinengine_mixin.py)?
Start at complete_task (in engine_mixin.py), then follow:
self.gain_xp(attr, xp_gain)Answer:
- Which stat dictionary changes?
- Why is a
whileloop used? - What does
gain_xpreturn?
Find this call in lifexp/ui_mixin.py:
self.create_quest_action_button(action_stack, "+", "Accept Quest", "accept", self.add_task_dialog)Answer:
- Which argument is the callback?
- Does the callback run immediately?
- What user action makes it run?
You are ready for the intermediate guide when you can explain these without memorizing:
- A list stores ordered items.
- A dictionary stores named values.
self.datais the app's main save structure.defcreates a reusable method.selfpoints to the current app object.ifchooses whether code runs.forrepeats over a collection.whilerepeats until a condition becomes false.returnsends a value back and stops a method.try/exceptprevents expected failures from crashing the app.with opensafely reads or writes files.- JSON turns save-file text into Python dictionaries and lists.
- Tkinter widgets create the UI.
- Tkinter callbacks run later when the user does something.
If two code blocks feel hard to connect, use the diagrams and read only one method at a time. The skill is not speed. The skill is tracing what Python does next.
























