Welcome to the ultimate guide for Flet developers! Whether you're just starting out or have been working with Flet for a while, you've probably encountered some frustrating issues. In this comprehensive guide, we'll explore 20+ common mistakes developers make when working with Flet, explain why they happen, and provide practical solutions with working code examples. Let's dive in and level up your Flet skills!
One of the most frequent beginner mistakes is modifying UI elements without updating the page. Flet won't
automatically reflect changes to your controls unless you explicitly call page.update(). This leads
to confusing situations where your code runs without errors, but the UI remains unchanged.
Always remember that Flet uses an imperative approach - you need to tell the framework when to refresh the display after making changes to your controls.
import flet as ft
def main(page: ft.Page):
counter = ft.Text("0")
def increment(e):
# This won't update the UI!
counter.value = str(int(counter.value) + 1)
# You MUST call this to see changes
page.update()
page.add(
counter,
ft.ElevatedButton("Increment", on_click=increment)
)
ft.app(target=main)
page.add() for initial controls and
page.update() for subsequent changes to existing controls.
Many developers get confused between storing a reference to a control versus creating a new instance. When you store a control in a variable, you're keeping a reference to that specific instance. Creating a new instance each time will result in unexpected behavior since you're not modifying the control that's actually displayed on the page.
This mistake often leads to "ghost controls" that exist in your code but aren't connected to the actual UI elements users interact with.
import flet as ft
def main(page: ft.Page):
# CORRECT: Store reference to the actual control
my_text = ft.Text("Original")
page.add(my_text)
def change_text(e):
my_text.value = "Changed!" # Modifies the displayed control
page.update()
# WRONG: Creating new instance
# def change_text_wrong(e):
# new_text = ft.Text("Changed!") # New instance, not connected to UI
# page.update() # Won't show anything
page.add(ft.ElevatedButton("Change Text", on_click=change_text))
ft.app(target=main)
Flet applications run in a single thread by default. If you perform long-running operations (like file processing, network requests, or complex calculations) directly in event handlers, your entire UI will freeze until the operation completes. This creates a terrible user experience where the application appears unresponsive.
The solution is to use Python's asyncio for asynchronous operations or run blocking code in separate
threads using concurrent.futures.
import flet as ft
import asyncio
import time
def main(page: ft.Page):
status = ft.Text("Ready")
page.add(status)
# BAD: This will freeze the UI for 5 seconds
# def bad_handler(e):
# time.sleep(5) # UI freezes!
# status.value = "Done!"
# page.update()
# GOOD: Using async/await
async def good_handler(e):
status.value = "Working..."
page.update()
await asyncio.sleep(5) # Non-blocking wait
status.value = "Done!"
page.update()
page.add(ft.ElevatedButton("Process", on_click=good_handler))
ft.app(target=main)
Flet event handlers must accept exactly one parameter (conventionally named e or event)
that represents the event object. Providing too few or too many parameters will cause runtime errors when the
event is triggered. Additionally, forgetting to make async event handlers async when using await
inside them will cause syntax errors.
Remember that even if you don't use the event object, you still need to include it in your function signature.
import flet as ft
def main(page: ft.Page):
# CORRECT signatures
def sync_handler(e): # Must have 'e' parameter
print("Button clicked!")
async def async_handler(e): # Async handler with 'e' parameter
await asyncio.sleep(1)
print("Async operation complete!")
# INCORRECT examples:
# def wrong_handler(): # Missing 'e' parameter - will crash!
# pass
# async def wrong_async_handler(): # Missing 'e' parameter
# pass
page.add(
ft.ElevatedButton("Sync", on_click=sync_handler),
ft.ElevatedButton("Async", on_click=async_handler)
)
ft.app(target=main)
Responsive design is crucial for modern applications, but many Flet developers forget to handle window resize
events. Without proper resize handling, your layout might break or look awkward on different screen sizes. Flet
provides the on_resize event for the page, but you need to implement logic to adjust your controls
accordingly.
Consider using responsive containers like ResponsiveRow or calculating dimensions dynamically based
on page.window_width and page.window_height.
import flet as ft
def main(page: ft.Page):
status = ft.Text(f"Size: {page.window_width}x{page.window_height}")
def handle_resize(e):
status.value = f"Size: {page.window_width}x{page.window_height}"
# Adjust layout based on new dimensions
if page.window_width < 600:
col.controls[0].width = page.window_width - 40
else:
col.controls[0].width = 500
page.update()
page.on_resize = handle_resize
col = ft.Column([
ft.Container(
content=ft.Text("Responsive Content"),
width=500,
bgcolor=ft.colors.BLUE_100,
padding=20
)
])
page.add(status, col)
ft.app(target=main)
When you add event listeners to controls that are later removed from the page, those listeners might continue to exist in memory, causing memory leaks. This is especially problematic in long-running applications or when frequently adding/removing controls dynamically.
Always clean up event listeners when controls are removed, or use weak references when possible. Flet doesn't automatically clean up event handlers when controls are removed from the page tree.
import flet as ft
def main(page: ft.Page):
dynamic_controls = []
def create_control():
text = ft.Text("Dynamic Item")
button = ft.ElevatedButton("Remove", on_click=lambda e: remove_control(text, button))
container = ft.Row([text, button])
dynamic_controls.append((text, button, container))
return container
def remove_control(text, button):
# Remove from page
for item in dynamic_controls:
if item[0] == text and item[1] == button:
page.controls.remove(item[2])
# Important: Clear references to prevent memory leaks
dynamic_controls.remove(item)
break
page.update()
page.add(
ft.ElevatedButton("Add Item", on_click=lambda e: page.add(create_control())),
ft.Divider()
)
ft.app(target=main)
Confusing padding and margin is a common layout mistake. Padding adds space inside a container (between the container's border and its content), while margin adds space outside the container (between the container and other elements). Using them incorrectly leads to unexpected spacing and alignment issues.
Remember: padding affects the container's internal space, margin affects external spacing. Also, Flet containers don't have a direct margin property - you need to use spacing in parent containers or wrap in another container with padding.
import flet as ft
def main(page: ft.Page):
# CORRECT usage
page.add(
ft.Container(
content=ft.Text("Inside Padding"),
padding=20, # Space inside container
bgcolor=ft.colors.AMBER_100
),
ft.Container(
content=ft.Text("More Content"),
padding=10,
bgcolor=ft.colors.GREEN_100
)
)
# To simulate margin, use Column spacing or wrap in another container
page.spacing = 15 # Space between controls in page
# Alternative: Wrap in container with padding for "margin" effect
# ft.Container(
# content=ft.Text("With margin-like spacing"),
# padding=ft.padding.only(top=15), # Top padding acts as margin
# bgcolor=ft.colors.RED_100
# )
ft.app(target=main)
When creating dynamic lists of controls (like chat messages, todo items, or data rows), failing to assign unique keys can cause serious UI issues. Flet uses keys to track which controls have changed, been added, or removed. Without keys, Flet might reuse controls incorrectly, leading to data being displayed in the wrong places or event handlers being attached to wrong items.
Always assign a unique, stable key to each item in a dynamic list. The key should remain the same for the same logical item across updates.
import flet as ft
def main(page: ft.Page):
items = []
next_id = 0
def add_item(e):
nonlocal next_id
item_id = next_id
next_id += 1
# CORRECT: Assign unique key
new_item = ft.ListTile(
key=str(item_id), # Unique key for this item
title=ft.Text(f"Item {item_id}"),
trailing=ft.IconButton(
ft.icons.DELETE,
on_click=lambda e, id=item_id: remove_item(id)
)
)
items.append(new_item)
update_list()
def remove_item(item_id):
global items
items = [item for item in items if item.key != str(item_id)]
update_list()
def update_list():
list_view.controls = items
page.update()
list_view = ft.ListView()
page.add(
ft.ElevatedButton("Add Item", on_click=add_item),
list_view
)
ft.app(target=main)
Hardcoding colors, fonts, and spacing throughout your application makes it difficult to maintain consistency and update the design later. Flet provides a powerful theming system that allows you to define consistent styles across your entire application.
Use page.theme and page.dark_theme to define global styles, and leverage Flet's
built-in color system instead of using raw hex values everywhere.
import flet as ft
def main(page: ft.Page):
# Define theme once
page.theme = ft.Theme(
color_scheme=ft.ColorScheme(
primary=ft.colors.BLUE,
secondary=ft.colors.GREEN,
background=ft.colors.GREY_100
),
font_family="Verdana"
)
# Use theme colors consistently
page.add(
ft.ElevatedButton("Primary Action", style=ft.ButtonStyle(color=ft.colors.ON_PRIMARY)),
ft.OutlinedButton("Secondary Action", style=ft.ButtonStyle(color=ft.colors.SECONDARY)),
ft.Container(
content=ft.Text("Themed Content"),
bgcolor=ft.colors.BACKGROUND,
padding=20
)
)
# Instead of hardcoded colors like:
# ft.ElevatedButton("Hardcoded", bgcolor="#2196F3")
ft.app(target=main)
Event handlers without proper error handling can cause your entire application to crash when unexpected situations occur. Network failures, invalid user input, file access issues, and other exceptions should be caught and handled gracefully to maintain application stability.
Always wrap potentially problematic code in try-except blocks and provide user feedback when errors occur. Never let unhandled exceptions bubble up from event handlers.
import flet as ft
def main(page: ft.Page):
status = ft.Text("")
page.add(status)
def risky_operation(e):
try:
# Simulate operation that might fail
result = 10 / 0 # This will raise ZeroDivisionError
status.value = f"Result: {result}"
except ZeroDivisionError:
status.value = "Error: Cannot divide by zero!"
status.color = ft.colors.RED
except Exception as ex:
status.value = f"Unexpected error: {str(ex)}"
status.color = ft.colors.RED
finally:
page.update()
page.add(ft.ElevatedButton("Perform Risky Operation", on_click=risky_operation))
ft.app(target=main)
Trying to create responsive layouts with basic Columns and Rows often leads to complex, hard-to-maintain code
with manual width calculations. Flet's ResponsiveRow control automatically handles responsive
behavior by wrapping controls to the next line when there's insufficient horizontal space.
Use col property on controls within ResponsiveRow to specify how many columns they should occupy on
different screen sizes.
import flet as ft
def main(page: ft.Page):
# CORRECT: Using ResponsiveRow
page.add(
ft.ResponsiveRow(
controls=[
ft.Container(
ft.Text("Card 1"),
col={"sm": 6, "md": 4, "xl": 3}, # Responsive columns
bgcolor=ft.colors.BLUE_100,
padding=10
),
ft.Container(
ft.Text("Card 2"),
col={"sm": 6, "md": 4, "xl": 3},
bgcolor=ft.colors.GREEN_100,
padding=10
),
ft.Container(
ft.Text("Card 3"),
col={"sm": 6, "md": 4, "xl": 3},
bgcolor=ft.colors.AMBER_100,
padding=10
),
ft.Container(
ft.Text("Card 4"),
col={"sm": 6, "md": 4, "xl": 3},
bgcolor=ft.colors.RED_100,
padding=10
)
],
spacing=10
)
)
# Instead of complex manual calculations with Columns/Rows
ft.app(target=main)
There's an important difference between page.add() and directly manipulating
page.controls. The page.add() method automatically calls page.update()
after adding controls, while modifying page.controls directly requires you to manually call
page.update() to see changes.
For adding multiple controls at once, it's more efficient to modify page.controls and then call
page.update() once, rather than calling page.add() multiple times.
import flet as ft
def main(page: ft.Page):
# Method 1: Using page.add() (automatically updates)
page.add(ft.Text("Added with page.add()"))
# Method 2: Direct manipulation (requires manual update)
page.controls.append(ft.Text("Added to controls list"))
page.update() # Must call this!
# Efficient for multiple controls:
new_controls = [
ft.Text("Batch item 1"),
ft.Text("Batch item 2"),
ft.Text("Batch item 3")
]
page.controls.extend(new_controls)
page.update() # Single update for all
ft.app(target=main)
Flet provides several page lifecycle events (on_connect, on_disconnect,
on_error) that are crucial for proper resource management. Not handling these events can lead to
memory leaks, unclosed connections, or inconsistent application state when users connect/disconnect.
Use these events to initialize resources when a user connects and clean up when they disconnect (especially important for web deployments with multiple users).
import flet as ft
def main(page: ft.Page):
user_id = None
def on_connect(e):
nonlocal user_id
user_id = id(e.page)
print(f"User connected: {user_id}")
# Initialize user-specific resources here
def on_disconnect(e):
print(f"User disconnected: {user_id}")
# Clean up user-specific resources here
# Close database connections, cancel timers, etc.
page.on_connect = on_connect
page.on_disconnect = on_disconnect
page.add(ft.Text("Lifecycle-aware application"))
ft.app(target=main)
As Flet applications grow in complexity, managing state across different parts of the UI becomes challenging. Storing state in local variables within the main function leads to spaghetti code that's hard to maintain and debug.
Consider implementing proper state management patterns like a centralized state class, observer pattern, or using Flet's built-in capabilities for more complex scenarios. This makes your code more predictable and easier to test.
import flet as ft
class AppState:
def __init__(self):
self.counter = 0
self.observers = []
def increment(self):
self.counter += 1
self.notify_observers()
def add_observer(self, observer):
self.observers.append(observer)
def notify_observers(self):
for observer in self.observers:
observer()
def main(page: ft.Page):
state = AppState()
counter_text = ft.Text(str(state.counter))
def update_counter():
counter_text.value = str(state.counter)
page.update()
state.add_observer(update_counter)
def handle_increment(e):
state.increment()
page.add(
counter_text,
ft.ElevatedButton("Increment", on_click=handle_increment)
)
ft.app(target=main)
While global variables might seem convenient for sharing state between functions, they create tight coupling, make testing difficult, and can lead to unpredictable behavior in multi-user scenarios (especially with Flet's web deployment).
Instead of globals, pass necessary data as parameters, use class-based approaches, or implement proper dependency injection. This makes your code more modular, testable, and maintainable.
import flet as ft
# BAD: Global variable
# current_user = None
def main(page: ft.Page):
# GOOD: Encapsulate state
class Session:
def __init__(self):
self.current_user = None
session = Session()
def login(e):
session.current_user = "john_doe"
update_ui()
def logout(e):
session.current_user = None
update_ui()
def update_ui():
if session.current_user:
welcome.value = f"Welcome, {session.current_user}!"
page.add(logout_btn)
if login_btn in page.controls:
page.controls.remove(login_btn)
else:
welcome.value = "Please log in"
page.add(login_btn)
if logout_btn in page.controls:
page.controls.remove(logout_btn)
page.update()
welcome = ft.Text("Please log in")
login_btn = ft.ElevatedButton("Login", on_click=login)
logout_btn = ft.ElevatedButton("Logout", on_click=logout)
page.add(welcome, login_btn)
ft.app(target=main)
Displaying large datasets (hundreds or thousands of items) by creating all controls at once can severely impact
performance and memory usage. Flet provides LazyColumn and virtualized lists that only render visible
items, dramatically improving performance.
For large datasets, always use virtualized scrolling controls instead of creating all items upfront. This keeps your application responsive even with massive amounts of data.
import flet as ft
def main(page: ft.Page):
# BAD: Creating all items at once
# items = [ft.ListTile(title=ft.Text(f"Item {i}")) for i in range(10000)]
# page.add(ft.ListView(controls=items))
# GOOD: Using virtualized list (pseudo-code - Flet doesn't have built-in yet)
# In practice, you'd implement pagination or use third-party solutions
# For demonstration, we'll show a paginated approach
current_page = 0
items_per_page = 50
total_items = 10000
def load_page(page_num):
start = page_num * items_per_page
end = min(start + items_per_page, total_items)
return [ft.ListTile(title=ft.Text(f"Item {i}")) for i in range(start, end)]
list_view = ft.ListView(height=400)
pagination = ft.Row()
def update_display():
list_view.controls = load_page(current_page)
pagination.controls = [
ft.IconButton(
ft.icons.CHEVRON_LEFT,
on_click=lambda e: change_page(-1),
disabled=current_page == 0
),
ft.Text(f"Page {current_page + 1}"),
ft.IconButton(
ft.icons.CHEVRON_RIGHT,
on_click=lambda e: change_page(1),
disabled=(current_page + 1) * items_per_page >= total_items
)
]
page.update()
def change_page(direction):
nonlocal current_page
current_page += direction
update_display()
update_display()
page.add(list_view, pagination)
ft.app(target=main)
Flet supports both synchronous and asynchronous programming models, but mixing them incorrectly can lead to
deadlocks, race conditions, or unexpected behavior. Remember that if your main function is async, you
must use await for async operations, and Flet will handle the async context properly.
Don't call async functions without await, and don't use await in synchronous functions. Choose one model consistently for your application.
import flet as ft
import asyncio
# CORRECT: Async main function
async def main(page: ft.Page):
status = ft.Text("Ready")
page.add(status)
async def long_operation():
await asyncio.sleep(2)
return "Operation completed"
async def handle_click(e):
status.value = "Working..."
page.update()
result = await long_operation() # Proper await
status.value = result
page.update()
page.add(ft.ElevatedButton("Start Async Operation", on_click=handle_click))
# Run with async support
ft.app(target=main)
# INCORRECT alternatives:
# def main_sync(page): # Sync main
# async def async_handler(e): # But async handler
# await asyncio.sleep(1) # This won't work properly
Just like in React, Flet applications can benefit from error boundaries - components that catch errors in their child components and display fallback UI instead of crashing the entire application. While Flet doesn't have built-in error boundaries, you can implement similar patterns.
Wrap critical sections of your UI in try-except blocks and provide fallback content when errors occur. This prevents isolated component failures from bringing down your entire application.
import flet as ft
def safe_container(content_func, fallback_text="Error loading content"):
try:
return content_func()
except Exception as e:
print(f"Component error: {e}")
return ft.Container(
content=ft.Column([
ft.Icon(ft.icons.ERROR, color=ft.colors.RED),
ft.Text(fallback_text, color=ft.colors.RED)
]),
padding=20,
bgcolor=ft.colors.RED_50
)
def main(page: ft.Page):
def risky_content():
# Simulate component that might fail
if False: # Change to True to see error handling
raise ValueError("Simulated component error")
return ft.Text("Content loaded successfully")
page.add(
ft.Text("Main Application"),
safe_container(risky_content)
)
ft.app(target=main)
Creating accessible applications is not just good practice - it's often a legal requirement. Many Flet developers forget to add proper accessibility attributes like semantic roles, labels, and keyboard navigation support.
Use Flet's accessibility properties (tooltip, expand, semantic roles) and ensure all
interactive elements are keyboard accessible. Test your application with screen readers and keyboard-only
navigation.
import flet as ft
def main(page: ft.Page):
# GOOD: Accessible controls
page.add(
ft.TextField(
label="Search",
hint_text="Enter search term",
tooltip="Search for products", # Helpful tooltip
autofocus=True
),
ft.ElevatedButton(
"Search",
tooltip="Click to search", # Descriptive tooltip
on_click=lambda e: print("Searching...")
),
ft.Switch(
label="Dark mode",
tooltip="Toggle dark mode theme" # Labels for switches
),
# Ensure all interactive elements can be reached with Tab key
ft.IconButton(
ft.icons.SETTINGS,
tooltip="Open settings", # Icons need tooltips
on_click=lambda e: print("Settings")
)
)
# BAD: Missing accessibility info
# ft.IconButton(ft.icons.SEARCH) # No tooltip, unclear purpose
ft.app(target=main)
Flet applications can run on web, desktop (Windows, macOS, Linux), and mobile. However, behavior can differ between platforms due to underlying differences in rendering engines, available fonts, and system capabilities.
Always test your application on all target platforms. Pay special attention to layout differences, font rendering, file system access, and platform-specific features. What works perfectly on Windows might have issues on macOS or in the browser.
import flet as ft
import platform
def main(page: ft.Page):
current_platform = platform.system()
# Platform-specific adjustments
if current_platform == "Darwin": # macOS
page.fonts = {"Default": "SF Pro"} # Use system font
elif current_platform == "Windows":
page.fonts = {"Default": "Segoe UI"}
# Web will use default browser fonts
page.add(
ft.Text(f"Running on: {current_platform}"),
ft.Text("Platform-specific content here"),
ft.ElevatedButton(
"Platform Action",
on_click=lambda e: handle_platform_action(current_platform)
)
)
def handle_platform_action(platform_name):
if platform_name == "Windows":
# Windows-specific logic
pass
elif platform_name == "Darwin":
# macOS-specific logic
pass
else:
# Web or other platforms
pass
ft.app(target=main)
Many developers confuse hiding a control (visible=False) with removing it from the page. Hidden
controls still exist in the control tree and consume memory, while removed controls are completely detached.
Choose the right approach based on your needs.
Use visibility toggling for controls you'll show/hide frequently. Remove controls entirely when they're no longer needed to free up memory and improve performance.
import flet as ft
def main(page: ft.Page):
secret_message = ft.Text("This is a secret!", visible=False)
def toggle_secret(e):
secret_message.visible = not secret_message.visible
page.update()
def remove_permanently(e):
if secret_message in page.controls:
page.controls.remove(secret_message)
toggle_btn.disabled = True
remove_btn.disabled = True
page.update()
toggle_btn = ft.ElevatedButton("Toggle Secret", on_click=toggle_secret)
remove_btn = ft.ElevatedButton("Remove Forever", on_click=remove_permanently)
page.add(
ft.Text("Secret Message Demo"),
secret_message,
toggle_btn,
remove_btn
)
ft.app(target=main)
Using print() statements for debugging works for simple cases, but becomes unmanageable in complex
applications. Flet applications, especially when deployed to web servers, benefit from proper logging with
different severity levels and structured output.
Implement proper logging with Python's logging module to track application flow, errors, and user
actions. This makes debugging production issues much easier.
import flet as ft
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("FletApp")
def main(page: ft.Page):
logger.info("Application started")
def handle_action(e):
try:
logger.debug("Action handler called")
# Simulate some logic
result = perform_action()
logger.info(f"Action completed successfully: {result}")
status.value = "Success!"
except Exception as ex:
logger.error(f"Action failed: {ex}", exc_info=True)
status.value = "Error occurred!"
finally:
page.update()
def perform_action():
# Simulate work
return "Done"
status = ft.Text("Ready")
page.add(
ft.ElevatedButton("Perform Action", on_click=handle_action),
status
)
ft.app(target=main)
Final Thoughts: Mastering Flet requires understanding both its unique architecture and general UI development best practices. By avoiding these common mistakes, you'll create more robust, maintainable, and user-friendly applications. Remember that every developer makes mistakes - the key is learning from them and continuously improving your code!