<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Archives des pattern-matching | dash-resources.com</title>
	<atom:link href="https://dash-resources.com/tag/pattern-matching/feed/" rel="self" type="application/rss+xml" />
	<link>https://dash-resources.com/tag/pattern-matching/</link>
	<description>Learn to build interactive web applications with Python and Dash plotly</description>
	<lastBuildDate>Thu, 06 Mar 2025 18:10:21 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.1</generator>

<image>
	<url>https://dash-resources.com/wp-content/uploads/2024/12/cropped-dash-logo-favicon-512-32x32.png</url>
	<title>Archives des pattern-matching | dash-resources.com</title>
	<link>https://dash-resources.com/tag/pattern-matching/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Build a To-Do app in Python with Dash (part 2/3)</title>
		<link>https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-2-3/</link>
		
		<dc:creator><![CDATA[Fran]]></dc:creator>
		<pubDate>Wed, 08 Jan 2025 19:05:17 +0000</pubDate>
				<category><![CDATA[Intermediate level]]></category>
		<category><![CDATA[pattern-matching]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://dash-resources.com/?p=434</guid>

					<description><![CDATA[<p>In part 1, we built a basic To-Do application with Dash and Dash Mantine Components (DMC). We created a single task list where users could [...]</p>
<p>L’article <a href="https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-2-3/">Build a To-Do app in Python with Dash (part 2/3)</a> est apparu en premier sur <a href="https://dash-resources.com">dash-resources.com</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In <a href="https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-1-3/">part 1</a>, we built a basic To-Do application with Dash and Dash Mantine Components (DMC). We created a single task list where users could add, modify, and delete tasks. However, our app had two major limitations:</p>



<ol class="wp-block-list">
<li>Tasks are lost when the page reloads</li>



<li>Users can only manage one list at a time</li>
</ol>



<p>In this tutorial, we&#8217;ll solve these problems by making tasks persistent using browser storage, and supporting multiple task lists by modifying the current callbacks.</p>



<p>Here&#8217;s a live display of the app you&#8217;ll learn to build (or <a href="https://scripts.dash-resources.com/todo_app/app5.py/">click here</a>):</p>



<iframe src="https://scripts.dash-resources.com/todo_app/app5.py/" width="100%" height="500" frameBorder="0"></iframe>



<p>Let&#8217;s dive in!</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id='part-1-making-tasks-persistent'  id="boomdevs_1" class="wp-block-heading" >Part 1: Making tasks persistent</h2>



<p>When we reload our current app, all tasks are reset to their initial state. This happens because Dash reinitializes all variables when the page reloads. To fix this, we need a way to save our tasks somewhere.</p>



<p>There are several options for persistence:</p>



<ul class="wp-block-list">
<li>Database (SQL, MongoDB, etc.)</li>



<li>File system</li>



<li>Browser storage</li>
</ul>



<p>For simplicity, we&#8217;ll use browser storage through Dash&#8217;s built-in <code>dcc.Store</code> component. This component can save data in the browser&#8217;s:</p>



<ul class="wp-block-list">
<li><code>memory</code>: Data is lost on page refresh</li>



<li><code>session</code>: Data persists until the browser tab is closed</li>



<li><code>local</code>: Data persists even after closing the browser</li>
</ul>



<p>As we want to keep tasks over the time,  <code>local</code> seems a perfect fit.</p>



<p class="callout"><strong>Note</strong>: using <code>local</code> means that the information is stored in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">localStorage</a> of the browser. It will stay as long as the user do not clean its browser / reset history.</p>



<h3 id='1-adding-storage-component'  id="boomdevs_2" class="wp-block-heading" >1. Adding storage component</h3>



<p>First, let&#8217;s modify our app to use <code>dcc.Store</code>. We&#8217;ll move our task list data into the store:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">app.layout = dmc.MantineProvider(
    [
        dmc.Container(
            get_list_layout(),  # No data passed here anymore
            size=400,
        ),
        dcc.Store("list_data_memory", data=sample_list_data, storage_type="local"),
        dcc.Store("current_index_memory", data=sample_list_data[0]["index"], storage_type="local"),
    ]
)
</code></pre>



<p>We wrap our layout in a list to include both the container and store.</p>



<p>The function<code>get_list_layout()</code> no longer receives data directly. Instead, a new callback will load the data from the <code>list_data_memory</code> directly to the components that need to be updated (list’ title, tasks, …).</p>



<p>Finally, we also add a <code>current_index_memory</code> to save the index of the currently displayed list.</p>



<h3 id='2-updating-the-layout-functions'  id="boomdevs_3" class="wp-block-heading" >2. Updating the layout functions</h3>



<p>Our <code>get_list_layout()</code> function needs to change since it won&#8217;t receive data directly:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_list_layout():
    """ Returns the list of checkboxes """

    content = dmc.Paper(
        [
            dmc.Title(id="main_list_title", order=2),

            dmc.Container(
                id="main_task_container",
                px=0,
                mt="md",
                mb="md",
            ),

            dmc.Button(
                "Add a new task",
                id="new_task_button",
                style={"width": "100%"},
                variant="outline",
                color="gray",
            )
        ],
        shadow="sm",
        p="md",
        mt="md",
        radius="sm",
    )

    return content
</code></pre>



<p>As you can see, we removed the <code>list_data</code> parameter. Instead of populating the layout with data, we add IDs to the title and task container. We will populate them using callbacks reading from <code>list_data_memory</code>.</p>



<h3 id='3-adding-update-callback'  id="boomdevs_4" class="wp-block-heading" >3. Adding update callback</h3>



<p>Let’s build a callback to update our container when the stored data changes:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("main_task_container", "children"),
    Output("main_list_title", "children"),
    Input("list_data_memory", "data"),
)
def update_task_container(list_data):
    """ Updates the list of tasks and list title"""

    return get_tasks_layout(list_data["tasks_list"]), list_data["title"]
</code></pre>



<p>This callback listens for changes in our stored data and updates both the task list and list title. that way, we’re sure that the layout will be updated every time when <code>list_data_memory</code> changes.</p>



<p>We set the <code>prevent_initial_call=False</code> (implicity) because we actually want this callback to populate the layout using the stored data from local storage at loading time.</p>



<h3 id='4-modifying-task-callbacks'  id="boomdevs_5" class="wp-block-heading" >4. Modifying task callbacks</h3>



<p>Now we need to update our task-related callbacks to work with stored data. Instead of directly modifying the layout, they&#8217;ll update the stored data:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("list_data_memory", "data", allow_duplicate=True),
    Input("new_task_button", "n_clicks"),
    State("list_data_memory", "data"),
    prevent_initial_call=True,
)
def add_task(n_clicks, list_data):
    """ Adds a task to the list """
    if not n_clicks:
        raise PreventUpdate

    # Create new task dictionary
    new_index = uuid.uuid4().hex
    new_task = {
        "index": new_index,
        "content": "",
        "checked": False,
    }

    # Add new task to the tasks list in memory
    list_data["tasks_list"].append(new_task)
    return list_data
</code></pre>



<p>The main changes are:</p>



<ol class="wp-block-list">
<li>Output is now the store data instead of the container</li>



<li>We take the current store data as State</li>



<li>We modify and return the stored data</li>
</ol>



<p>We&#8217;ll do the same for the remove task callback:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("list_data_memory", "data", allow_duplicate=True),
    Input({"type": "task_del", "index": ALL}, "n_clicks"),
    State("list_data_memory", "data"),
    prevent_initial_call=True,
)
def remove_task(n_clicks, list_data):
    """ Remove a task from the list """
    if not any(n_clicks):
        raise PreventUpdate

    task_index = ctx.triggered_id["index"]

    # Find and remove the task with matching index
    list_data["tasks_list"] = [
        task for task in list_data["tasks_list"]
        if task["index"] != task_index
    ]

    return list_data
</code></pre>



<p>Apart from the fact that we can persist the data, this way of updating a <code>dcc.Store</code> is a better, cleaner way than relying directly on the container. We have more control on the stored data, on its form and when it is updated.</p>



<h3 id='5-adding-task-update-callback'  id="boomdevs_6" class="wp-block-heading" >5. Adding task update callback</h3>



<p>We also need to handle updating task content and checked status:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("list_data_memory", "data", allow_duplicate=True),
    Input({"type": "task_checked", "index": ALL}, "checked"),
    Input({"type": "task_content", "index": ALL}, "value"),
    State("list_data_memory", "data"),
    prevent_initial_call=True,
)
def update_task_checked(checked_values, content_values, list_data):
    """Updates the checked state and content of tasks"""
    if not checked_values:
        raise PreventUpdate

    # Find the index position in our list of tasks
    task_index = ctx.triggered_id["index"]
    task_pos = [task["index"] for task in list_data["tasks_list"]].index(task_index)

    task_checked_value = checked_values[task_pos]
    task_content_value = content_values[task_pos]

    # Update the task values in list_data
    list_data["tasks_list"][task_pos]["checked"] = task_checked_value
    list_data["tasks_list"][task_pos]["content"] = task_content_value

    return list_data
</code></pre>



<h3 id='6-run-the-app'  id="boomdevs_7" class="wp-block-heading" >6. Run the app</h3>



<p>Now our tasks persist across page reloads! You can try it by adding some tasks and reloading this page, you tasks should still be here <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f389.png" alt="🎉" class="wp-smiley" style="height: 1em; max-height: 1em;" />.</p>



<iframe src="https://scripts.dash-resources.com/todo_app/app3.py/" width="100%" height="500" frameBorder="0"></iframe>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/27a1.png" alt="➡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/Spriteware/todo-app-python-dash/blob/master/app3.py">See the full code on Github</a> or <a href="https://scripts.dash-resources.com/todo_app/app3.py/">open the app</a>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id='part-2-supporting-multiple-lists'  id="boomdevs_8" class="wp-block-heading" >Part 2: Supporting multiple lists</h2>



<p>You will be surprised how easy it is now to handle the multiple list. As we have a <em>store</em> <em>→</em> <em>layout</em> scheme, we only need to modify the way that the data is structured and modify the layout to add this feature.</p>



<h3 id='1-updated-data-structure'  id="boomdevs_9" class="wp-block-heading" >1. Updated data structure</h3>



<p>First, let&#8217;s change our data structure to support multiple lists:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">sample_list_data = [
    {
        "index": uuid.uuid4().hex,
        "title": "My Tasks",
        "tasks_list": [
            {
                "index": uuid.uuid4().hex,
                "content": "Task A",
                "checked": True,
            },
            # ... other tasks
        ],
    },
    {
        "index": uuid.uuid4().hex,
        "title": "Shopping list",
        "tasks_list": [],
    }
]
</code></pre>



<p>As you can see, we simply wrap the old <code>dict</code> inside a list, and add another example. We also anticipate and add a unique ID (<code>index</code>) because soon or later we will need to identify lists from each other.</p>



<h3 id='2-adding-list-navigation'  id="boomdevs_10" class="wp-block-heading" >2. Adding list navigation</h3>



<p>Let’s add a sidebar for list navigation. Each list will be represented as a card with a title and a progression bar.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img fetchpriority="high" decoding="async" width="1024" height="540" src="https://dash-resources.com/wp-content/uploads/2025/01/image-2-1024x540.png" alt="Illustration of the navigation sidebar with title and progress bar for each task list." class="wp-image-436" style="width:699px;height:auto" srcset="https://dash-resources.com/wp-content/uploads/2025/01/image-2-1024x540.png 1024w, https://dash-resources.com/wp-content/uploads/2025/01/image-2-300x158.png 300w, https://dash-resources.com/wp-content/uploads/2025/01/image-2-768x405.png 768w, https://dash-resources.com/wp-content/uploads/2025/01/image-2-1536x810.png 1536w, https://dash-resources.com/wp-content/uploads/2025/01/image-2-1320x696.png 1320w, https://dash-resources.com/wp-content/uploads/2025/01/image-2.png 1628w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Illustration of the navigation sidebar with title and progress bar for each task list.</figcaption></figure>
</div>


<p>A new function <code>get_list_navigation_layout</code> will handle this:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_list_navigation_layout(list_data, current_index):
    """ Returns a list of lists titles and progressions """

    items = []

    for list_item in list_data:
        # Compute progression
        progress_value = get_progression(list_item)
        progress_color = "green" if progress_value == 100 else "blue"

        # Build card element
        elem = dmc.Paper(
            html.A(
                [
                    dmc.Title(list_item["title"], order=4, mb="sm"),
                    dmc.Progress(value=progress_value, color=progress_color),
                ],
                id={"type": "list_button", "index": list_item["index"]},
                style={"cursor": "pointer"},
            ),
            p="xs",
            mb="sm",
            withBorder=True,
            className="active" if current_index == list_item["index"] else ""
        )
        items.append(elem)

    return items

def get_progression(list_item):
    """ Computes the progression of a list """
    tasks = list_item["tasks_list"]
    if len(tasks) == 0:
        return 0
    return len([task for task in tasks if task["checked"]]) / len(tasks) * 100
</code></pre>



<p>We simply iterate over the lists in <code>list_data</code>. For each list, we compute the progression (the progress bar turns green at 100% instead of blue) and make the cards with title and progress bar.</p>



<p class="callout"><strong>Notice</strong> that we wrapped the content inside a <code>html.A</code> button. That way, we make the card clickable and we will be able to create a callback listening for clicks (<code>n_clicks</code>) on the card component.</p>



<p>The <code>current_index</code> parameter will store the <code>index</code> of the list we’re currently displaying. We use it to set the CSS class “active” to this list, making it focused compared to the others.</p>



<pre class="wp-block-code"><code lang="css" class="language-css">/* Filepath: assets/style.css */
/* ... (previous code) ... */

#list_navigation_layout &gt; div {
    opacity: 0.7; 
}

/* Opacity is a good way to emphasize some elements */
#list_navigation_layout &gt; div.active {
    border-color: black;
    opacity: 1.0;
} 
</code></pre>



<p>We also need a &#8220;New list&#8221; button:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_new_list_button():
    return dmc.Button(
        "New list",
        id="new_list_button",
        style={"width": "100%"},
        color="black",
        mt="md",
        mb="md"
    )
</code></pre>



<h3 id='3-updated-app-layout'  id="boomdevs_11" class="wp-block-heading" >3. Updated app layout</h3>



<p>Finally, we use DMC’s grid system to create a two-panel layout:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">app.layout = dmc.MantineProvider(
    [
        dmc.Container(
            dmc.Grid(
                [
                    dmc.GridCol(
                        [
                            get_new_list_button(),  # The new list button
                            dmc.Container(          # This container will be updated dynamically
                                id="list_navigation_layout",
                                px=0,
                            )
                        ],
                        span=4,
                    ),
                    dmc.GridCol(
                        get_list_layout(),
                        span="auto",
                        ml="xl"
                    ),
                ],
                gutter="md"
            ),
            size=600,
        ),
        
        # The stored data in localStorage
        dcc.Store("list_data_memory", data=sample_list_data, storage_type="local"),
        dcc.Store("current_index_memory", data=sample_list_data[0]["index"], storage_type="local"),
    ]
)
</code></pre>



<p>The left panel serves as a navigation sidebar, displaying all available lists with their progress bars. The right panel shows the tasks of the currently selected list, maintaining our familiar task interface but adding an editable title and a list deletion option.</p>



<p>Here’s the interactive version of the new layout:</p>



<iframe loading="lazy" src="https://scripts.dash-resources.com/todo_app/app4.py/" width="100%" height="500" frameBorder="0"></iframe>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/27a1.png" alt="➡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/Spriteware/todo-app-python-dash/blob/master/app4.py">See the full code on Github</a> or <a href="https://scripts.dash-resources.com/todo_app/app4.py/">open the app</a>.</p>



<p>In the next section, we&#8217;ll add the necessary callbacks to make this layout interactive.</p>



<h3 id='4-list-management-callbacks'  id="boomdevs_12" class="wp-block-heading" >4. List management callbacks</h3>



<p>With multiple lists to manage, we need callbacks to handle list-level operations.</p>



<p>The list switching callback responds to clicks in the navigation sidebar, updating <code>current_index_memory</code> to display the selected list:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("current_index_memory", "data", allow_duplicate=True),
    Input({"type": "list_button", "index": ALL}, "n_clicks"),
    prevent_initial_call=True,
)
def switch_list(n_clicks):
    """ Changes the current displayed list"""
    if not any(n_clicks):
        raise PreventUpdate

    list_index = ctx.triggered_id["index"]
    return list_index
</code></pre>



<p>When users create a new list, the <code>add_list</code> callback adds it to our collection and automatically makes it the current list by updating <code>current_index_memory</code>.</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    [
        Output("list_data_memory", "data", allow_duplicate=True),
        Output("current_index_memory", "data"),
    ],
    Input("new_list_button", "n_clicks"),
    State("list_data_memory", "data"),
    prevent_initial_call=True,
)
def add_list(n_clicks, list_data):
    """ Add a new list and display it"""
    if not n_clicks:
        raise PreventUpdate

    new_index = uuid.uuid4().hex
    new_list = {
        "index": new_index,
        "title": "New list",
        "tasks_list": [],
    }

    list_data.append(new_list)
    return list_data, new_index
</code></pre>



<p>You’ll notice how the code is similar to the <code>add_task</code> callback.</p>



<p>List deletion works in two steps: first showing a confirmation modal, then removing the list if confirmed. That way, the UX (User Experience) is better and avoids mistakingly deleting lists! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("del_list_modal", "opened"),
    [
        Input("del_list_button", "n_clicks"),
        Input("del_list_modal_confirm_button", "n_clicks"),
    ],
    prevent_initial_call=True,
)
def delete_modal_open_close(n_clicks, n_clicks2):
    """ Open or closes modal """
    if not n_clicks:
        raise PreventUpdate

    if ctx.triggered_id == "del_list_modal_confirm_button":
        return False

    return True

@app.callback(
    [
        Output("list_data_memory", "data", allow_duplicate=True),
        Output("current_index_memory", "data", allow_duplicate=True),
    ],
    [
        Input("del_list_modal_confirm_button", "n_clicks"),
        State("list_data_memory", "data"),
        State("current_index_memory", "data"),
    ],
    prevent_initial_call=True,
)
def delete_list(n_clicks, list_data, current_index):
    """ Updates the current list title """
    if not n_clicks:
        raise PreventUpdate

    # Remove the current list
    i = get_pos_from_index(list_data, current_index)
    del list_data[i]

    # Focus again on the first index if there are notes
    current_index = list_data[0]["index"] if len(list_data) &gt; 0 else None
    return list_data, current_index

</code></pre>



<p>We used <code>ctx.triggered_id</code> to differentiate whether we needed to open or close the modal. It’s a simple way to update the same component without relying on <code>allow_duplicate=True</code> and making two separate callbacks.</p>



<p class="callout"><strong>Note</strong>: you might wonder why the Outputs and Inputs are now wrapped inside lists: <code>[Output(…), Output(…)]</code>. In this case, this is just syntax sugar that helps visualizing what are the inputs / outputs easily. I often do this as the outputs or inputs grows.</p>



<p>We&#8217;ve added a helper function <code>get_pos_from_index</code> that simplifies finding lists in our data structure &#8211; it&#8217;s used throughout our callbacks to ensure we&#8217;re modifying the correct list:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_pos_from_index(dict_list, index):
    """ Gets position of dict with matching index in a list
			  Example:
	      * get_pos_from_index([{"index": "abc"}, {"index": "def"}], "def")  
	      * returns 1
    """
    for i, elem in enumerate(dict_list):
        if elem["index"] == index:
            return i
    return None
</code></pre>



<p>The helper work both for lists and for tasks.</p>



<h3 id='5-updated-task-callbacks'  id="boomdevs_13" class="wp-block-heading" >5. Updated task callbacks</h3>



<p>Finally, we need to update our task callbacks to work with the currently displayed list (using <code>current_index_memory</code>):</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("list_data_memory", "data", allow_duplicate=True),
    [
        Input("new_task_button", "n_clicks"),
        State("list_data_memory", "data"),
        State("current_index_memory", "data"),
    ],
    prevent_initial_call=True,
)
def add_task(n_clicks, list_data, current_index):
    """ Adds a task to the list """
    if not n_clicks:
        raise PreventUpdate

    # Create new task dictionary
    new_index = uuid.uuid4().hex
    new_task = {
        "index": new_index,
        "content": "",
        "checked": False,
    }

    # Add new task to the tasks list in memory
    i = get_pos_from_index(list_data, current_index)
    list_data[i]["tasks_list"].append(new_task)

    return list_data
</code></pre>



<p>Similar updates are needed for the remove and update task callbacks. The key change is using <code>get_pos_from_index()</code> to find the current list in our data and updating it.</p>



<h3 id='6-run-the-app-1'  id="boomdevs_14" class="wp-block-heading" >6. Run the app</h3>



<p>Here is the final result. Try switch from a list to another, add list, remove, modify tasks… And reload the page!</p>



<p>Here’s the interactive version of the new layout:</p>



<iframe src="https://scripts.dash-resources.com/todo_app/app5.py/" width="100%" height="500" frameBorder="0"></iframe>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/27a1.png" alt="➡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/Spriteware/todo-app-python-dash/blob/master/app5.py">See the full code on Github</a> or <a href="https://scripts.dash-resources.com/todo_app/app5.py/">open the app</a>.</p>



<p>You can also download the entire project below:</p>



                <div class="ml-embedded" data-form="LGDFB1"></div>
            



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id='conclusion'  id="boomdevs_15" class="wp-block-heading" >Conclusion</h2>



<p>Throughout this tutorial, we&#8217;ve taken our basic task list and transformed it into a more powerful application. We could go even further: adding user login/register, share task lists, etc. Feel free to implement them on your side! </p>



<p>In this tutorial we’ve covered key concepts:</p>



<ul class="wp-block-list">
<li>Using browser storage with <code>dcc.Store</code></li>



<li>Managing complex state across multiple components</li>



<li>Building modular layouts with DMC Grid</li>



<li>Handling nested data structures in callbacks</li>
</ul>



<p>In the final part of this series, we&#8217;ll look at improving performance using Dash&#8217;s Patch system.</p>



<p>You can find the complete code for this tutorial on Github: <a href="https://github.com/Spriteware/todo-app-python-dash">https://github.com/Spriteware/todo-app-python-dash</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>I hope you enjoyed this tutorial. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p>You can ask questions on the associated topic on Plotly’s community forum: <a href="https://community.plotly.com/t/tutorial-build-a-to-do-app-in-python-with-dash/89805">here</a>.<br><strong>Be sure to subscribe to the newsletter to see the next articles!</strong></p>



<p></p>
<p>L’article <a href="https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-2-3/">Build a To-Do app in Python with Dash (part 2/3)</a> est apparu en premier sur <a href="https://dash-resources.com">dash-resources.com</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Build a To-Do app in Python with Dash (part 1/3)</title>
		<link>https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-1-3/</link>
		
		<dc:creator><![CDATA[Fran]]></dc:creator>
		<pubDate>Wed, 08 Jan 2025 18:58:49 +0000</pubDate>
				<category><![CDATA[Beginner level]]></category>
		<category><![CDATA[pattern-matching]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://dash-resources.com/?p=409</guid>

					<description><![CDATA[<p>In this tutorial, we&#8217;ll build a To-Do application 100% in Python using Dash plotly and the community extension Dash Mantine Components. We&#8217;ll take an iterative [...]</p>
<p>L’article <a href="https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-1-3/">Build a To-Do app in Python with Dash (part 1/3)</a> est apparu en premier sur <a href="https://dash-resources.com">dash-resources.com</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In this tutorial, we&#8217;ll build a To-Do application 100% in Python using Dash plotly and the community extension Dash Mantine Components. We&#8217;ll take an iterative approach, starting with the basics and gradually adding complexity as we understand why each piece is needed.</p>



<p>The tutorial is in three parts:</p>



<ol class="wp-block-list">
<li>Setup the layout, handle a minimal task list (part 1 &#8211; this article)</li>



<li><a href="https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-2-3/">Handle multiple lists and save tasks on page reload (part 2)</a></li>



<li><a href="https://dash-resources.com/writing-in-process/">Improve scalability and performance with Patch (part 3)</a></li>
</ol>



<p>At the end of this article, you’ll know how to build the following To-Do app with Dash python:</p>



<iframe loading="lazy" src="https://scripts.dash-resources.com/todo_app/app2.py/" width="100%" height="400" frameBorder="0"></iframe>



<p>Let’s go!</p>



<h2 id='introduction-to-dash-mantine-components'  id="boomdevs_1" class="wp-block-heading" >Introduction to Dash Mantine Components</h2>



<p>Before we dive in, let&#8217;s understand what <a href="https://www.dash-mantine-components.com/">Dash Mantine Components</a> (DMC) offers us. DMC is a component library that provides pre-built React components with a modern design system. It includes layout helpers (e.g. grid systems, centered container, &#8230;) and out-of-the-box components (e.g. Modal, Radio Button, Slider, etc.) </p>



<figure class="wp-block-image size-full is-style-default"><img loading="lazy" decoding="async" width="1796" height="684" src="https://dash-resources.com/wp-content/uploads/2025/01/image.png" alt="Illustration: a modal is a typical example of what DMC can provide out-of-the-box." class="wp-image-410" srcset="https://dash-resources.com/wp-content/uploads/2025/01/image.png 1796w, https://dash-resources.com/wp-content/uploads/2025/01/image-300x114.png 300w, https://dash-resources.com/wp-content/uploads/2025/01/image-1024x390.png 1024w, https://dash-resources.com/wp-content/uploads/2025/01/image-768x292.png 768w, https://dash-resources.com/wp-content/uploads/2025/01/image-1536x585.png 1536w, https://dash-resources.com/wp-content/uploads/2025/01/image-1320x503.png 1320w" sizes="auto, (max-width: 1796px) 100vw, 1796px" /><figcaption class="wp-element-caption">Illustration: a modal is a typical example of what DMC can provide out-of-the-box.</figcaption></figure>



<p>Some key components we&#8217;ll use:</p>



<ul class="wp-block-list">
<li><code>dmc.Container</code>: A centered container with max-width</li>



<li><code>dmc.Paper</code>: A white background container with optional shadow</li>



<li><code>dmc.Grid</code> and <code>dmc.GridCol</code>: Flexbox-based grid system</li>



<li><code>dmc.Button</code>, <code>dmc.Checkbox</code>, <code>dmc.ActionIcon</code>: Interactive components</li>
</ul>



<p>DMC uses several spacing and positioning attributes:</p>



<ul class="wp-block-list">
<li><code>mt</code>: margin-top (e.g., <code>mt="md"</code> for medium margin-top)</li>



<li><code>mb</code>: margin-bottom</li>



<li><code>px</code>: padding on x-axis (left and right)</li>



<li><code>p</code>: padding on all sides</li>



<li>Size values: &#8220;xs&#8221;, &#8220;sm&#8221;, &#8220;md&#8221;, &#8220;lg&#8221;, &#8220;xl&#8221; or numeric pixels</li>
</ul>



<p>This will be useful for the rest of the article. If you encounter something you don’t understand, just go back here or search the component in <a href="https://www.dash-mantine-components.com/">DMC documentation</a>.</p>



<h2 id='step-1-setting-up-the-layout'  id="boomdevs_2" class="wp-block-heading" >Step 1: Setting up the layout</h2>



<h3 id='1-data-structure'  id="boomdevs_3" class="wp-block-heading" >1. Data structure</h3>



<p>Let&#8217;s start by defining how we&#8217;ll store our task data. This will actually help us shape the layout.</p>



<p>We want to create and save tasks. Each task should have a text content, and a checkbox status. That means having something like:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">sample_list_data = {
    "title": "My Tasks",
    "tasks_list": [
        {
            "content": "Task A",
            "checked": True,
        },
        {
            "content": "Task B",
            "checked": False,
        },
        {
            "content": "Task C",
            "checked": False,
        },
    ],
}

</code></pre>



<p>From this structure, we can build the layout function to display tasks.</p>



<h3 id='2-creating-reusable-layout-functions'  id="boomdevs_4" class="wp-block-heading" >2. Creating reusable layout functions</h3>



<p>A key aspect of our design is creating separate functions for each part of the layout. This isn&#8217;t just for code organization &#8211; it&#8217;s crucial for the dynamic nature of our app. Later, when we add or remove tasks, we&#8217;ll need to recreate portions of the layout dynamically. By having these functions, we can easily generate new task elements or update existing ones.</p>



<p>Let&#8217;s start with a function to render a single task using DMC&#8217;s Grid system:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_task(task_dict):
    """ Returns a single task layout """
    text = task_dict["content"]
    checked = task_dict["checked"]

    content = dmc.Grid(
        [
            # Checkbox column
            dmc.GridCol(
                dmc.Checkbox(
                    checked=checked,
                    mt=2  # Align checkbox vertically
                ),
                span="content"  # Take only needed space
            ),
            # Task text column - Using Input for editability
            dmc.GridCol(
                dmc.Text(  # Wrap in Text component for consistent styling
                    dcc.Input(
                        text,
                        className="shadow-input",  # Custom styling for input
                        debounce=True              # For callbacks
                    )
                ),
                span="auto"  # Take remaining space
            ),
            # Delete button column
            dmc.GridCol(
                dmc.ActionIcon(
                    DashIconify(icon="tabler:x", width=20),
                    variant="transparent",
                    color="gray",
                    className="task-del-button"
                ),
                span="content"
            ),
        ],
        className="task-container"
    )

    return content

</code></pre>



<p>We used <code>dmc.Grid</code> to easily get a 3 column structure. The first column has the checkbox, the second the text content, and the third has the delete task button.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="836" height="624" src="https://dash-resources.com/wp-content/uploads/2025/01/image-1.png" alt="Illustration for the 3 column grid structure." class="wp-image-411" style="width:389px;height:auto" srcset="https://dash-resources.com/wp-content/uploads/2025/01/image-1.png 836w, https://dash-resources.com/wp-content/uploads/2025/01/image-1-300x224.png 300w, https://dash-resources.com/wp-content/uploads/2025/01/image-1-768x573.png 768w" sizes="auto, (max-width: 836px) 100vw, 836px" /><figcaption class="wp-element-caption">Illustration for the 3 column grid structure.</figcaption></figure>
</div>


<p>You might wonder why we use a <code>dcc.Input</code> wrapped in a <code>dmc.Text</code> component. The idea is that we want to modify the text just by clicking on it. The most similar example is the <a href="https://developer.mozilla.org/fr/docs/Web/HTML/Global_attributes/contenteditable">“contenteditable” elements</a> in HTML : but they aren’t available in Dash.</p>



<p>Instead, we will style the app with some CSS to make the input look like normal text when it is not focused (i.e. being modified).</p>



<p class="callout"><strong>Note</strong>: the <code>debounce</code> attribute is used to trigger the input “update” event when the user finishes typing, not every time a key is pressed. In this case, it offers a better user experience.</p>



<h3 id='3-creating-the-tasks-list-and-main-container'  id="boomdevs_5" class="wp-block-heading" >3. Creating the tasks list and main container</h3>



<p>Now let&#8217;s create a function to render all tasks. It’s simply the for loop over the <code>get_task</code> function:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_tasks_layout(tasks_list):
    """ Returns the list of tasks """
    tasks = []
    for task_dict in tasks_list:
        task_layout = get_task(task_dict)
        tasks.append(task_layout)
    return tasks

</code></pre>



<p>We&#8217;ll wrap everything in a main container with a title and &#8220;Add a new ask&#8221; button:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_list_layout(list_data):
    """ Returns the list container with title and tasks """
    tasks_layout = get_tasks_layout(list_data["tasks_list"])

    content = dmc.Paper(
        [
            dmc.Title(list_data["title"], order=2),

            # Tasks container
            dmc.Container(
                tasks_layout,
                id="main_task_container",
                px=0,  # No horizontal padding
                mt="md",  # Medium margin top
                mb="md",  # Medium margin bottom
            ),

            # Add task button
            dmc.Button(
                "Add a new task",
                id="new_task_button",
                style={"width": "100%"},
                variant="outline",
                color="gray",
            )
        ],
        shadow="sm",  # Light shadow
        p="md",       # Medium padding
        mt="md",      # Medium margin top
        radius="sm",  # Slightly rounded corners
    )

    return content

</code></pre>



<h3 id='4-styling-with-css'  id="boomdevs_6" class="wp-block-heading" >4. Styling with CSS</h3>



<p>DMC already brings a set of default stylesheet, which make us gain a lot of time. But as said previously, we want our we want our task inputs to look clean and minimal, with appropriate visual feedback for user interactions:</p>



<p>So we create a <code>style.css</code> file in the <code>assets/</code> folder. This will be loaded automatically by Dash:</p>



<pre class="wp-block-code"><code lang="css" class="language-css">/* 
 * Filepath: ./assets/style.css 
 */

.shadow-input {
    display: inline-block;
    width: 100%;
    margin: 0;
    border: 1px solid black;
    border-color: transparent;
    border-radius: 5px;
}
.shadow-input:hover {
    border-color: gray;
}

.shadow-input:active, .shadow-input:focus {
    border-color: black;
}

.task-container:has(.mantine-Checkbox-root[data-checked]) .mantine-Text-root input {
    text-decoration: line-through;
    color: gray;
}

.task-del-button:hover {
    color: red;
}
</code></pre>



<p>This CSS does several things:</p>



<ol class="wp-block-list">
<li>Makes inputs blend seamlessly into the layout with transparent borders</li>



<li>Shows subtle borders on hover and focus for better UX</li>



<li>Applies strikethrough styling to checked tasks</li>



<li>Adds a red hover effect to delete buttons</li>
</ol>



<p>A key design decision here is handling the strikethrough effect with CSS rather than callbacks. By using the <code>:has()</code> selector to detect checked checkboxes, we can apply the strikethrough style directly in CSS. This is more efficient than using a callback, as it eliminates unnecessary round-trips to the server and provides instant visual feedback.</p>



<h3 id='5-running-the-app'  id="boomdevs_7" class="wp-block-heading" >5. Running the app</h3>



<p>Now let&#8217;s put everything together and start our application:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">import dash_mantine_components as dmc
from dash import Dash, _dash_renderer, dcc
from dash_iconify import DashIconify
_dash_renderer._set_react_version("18.2.0")  # for mantine

# sample_list_data = ...

## Layout functions:
# get_task()
# get_tasks_layout()
# get_list_layout

app = Dash(__name__)

app.layout = dmc.MantineProvider(
    dmc.Container(
        get_list_layout(sample_list_data),
        size=400,  # Container width
    )
)

# Start the server
if __name__ == '__main__':
    app.run_server(debug=True)

</code></pre>



<p>When you run this code, you&#8217;ll have a basic task list application without interactivity yet:</p>



<iframe loading="lazy" src="https://scripts.dash-resources.com/todo_app/app1.py/" width="100%" height="400" frameBorder="0"></iframe>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/27a1.png" alt="➡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/Spriteware/todo-app-python-dash/blob/master/app1.py">See the full code on Github</a> or <a href="https://scripts.dash-resources.com/todo_app/app1.py/">open the app</a>.</p>



<p>Now that we have our layout defined, we can make it interactive with callbacks.</p>



<h2 id='step-2-adding-interactivity-with-callbacks'  id="boomdevs_8" class="wp-block-heading" >Step 2: Adding interactivity with callbacks</h2>



<p>Now comes the interesting part. We need to make our tasks interactive &#8211; we need to handle creating, reading, updating, and deleting tasks (<a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD operations</a>). However, we face an interesting challenge: how do we handle callbacks for elements that don&#8217;t exist when the app starts?</p>



<p>In regular Dash callbacks, you must define both inputs and outputs statically &#8211; they need to exist when the app initializes. This works fine for static elements like a single button or dropdown. But in our task app, we&#8217;re dynamically adding and removing tasks! These elements don&#8217;t exist when the app starts running.</p>



<p>This is where pattern-matching callbacks come in. Instead of targeting specific IDs like &#8220;button-1&#8221; or &#8220;task-input-2&#8221;, pattern-matching callbacks let us define patterns that match multiple components, even ones created after the app starts running. They work by using a dictionary-based ID system and special selectors like <code>ALL</code>.</p>



<p>For example, compare these two approaches:</p>



<pre class="wp-block-code"><code lang="python" class="language-python"># Regular callback - Only works for a specific, existing button
@app.callback(
    Output("static-div", "children"),
    Input("submit-button", "n_clicks")
)

# Pattern-matching callback - Works for ANY button matching the pattern
@app.callback(
    Output({"type": "result", "id": ALL}, "children"),
    Input({"type": "submit", "id": ALL}, "n_clicks")
)
</code></pre>



<p>The pattern-matching version uses a dictionary for the ID with two keys:</p>



<ul class="wp-block-list">
<li>&#8220;type&#8221;: Groups similar components (like all submit buttons)</li>



<li>&#8220;id&#8221;: Uniquely identifies each instance</li>
</ul>



<p>Now we can write a single callback that handles all tasks of the same type, even ones created dynamically after the app starts! When we add a new task with ID <code>{"type": "task", "id": "123"}</code>, the callback will automatically work for it.</p>



<p>This is perfect for our task app where we need to add and remove tasks dynamically. Let’s continue.</p>



<p class="callout"><strong>Learn more</strong> on Pattern-Matching callbacks from the Dash plotly documentation: <a href="https://dash.plotly.com/pattern-matching-callbacks">https://dash.plotly.com/pattern-matching-callbacks</a></p>



<h3 id='1-adding-unique-identifiers'  id="boomdevs_9" class="wp-block-heading" >1. Adding unique identifiers</h3>



<p>As Pattern-Matching callbacks need a unique id, we need to modify our data structure to include unique identifiers for each task. Let&#8217;s add an <code>index</code> field using UUID :</p>



<pre class="wp-block-code"><code lang="python" class="language-python">import uuid

sample_list_data = {
    "title": "My Tasks",
    "tasks_list": [
        {
            "index": uuid.uuid4().hex,  # Add unique identifier
            "content": "Task A",
            "checked": True,
        },
        # ... other tasks
    ],
}

</code></pre>



<p>Example UUID: <code>f4da57942cec46b7ba448c88fad11996</code>. We could as well have used integers. It doesn’t matter as long as the ids are unique.</p>



<p>Now we need to update our <code>get_task</code> function to use these identifiers:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">def get_task(task_dict):
    """ Returns a single task layout """
    text = task_dict["content"]
    checked = task_dict["checked"]
    index = task_dict["index"]  # Get the index

    content = dmc.Grid(
        [
            dmc.GridCol(
                dmc.Checkbox(
                    id={"type": "task_checked", "index": index},  # Add ID
                    checked=checked,
                    mt=2
                ),
                span="content"
            ),
            dmc.GridCol(
                dmc.Text(
                    dcc.Input(
                        text,
                        id={"type": "task_content", "index": index},  # Add ID
                        className="shadow-input",
                        debounce=True,
                    )
                ),
                span="auto"
            ),
            dmc.GridCol(
                dmc.ActionIcon(
                    DashIconify(icon="tabler:x", width=20),
                    id={"type": "task_del", "index": index},  # Add ID
                    variant="transparent",
                    color="gray",
                    className="task-del-button"
                ),
                span="content"
            ),
        ],
        className="task-container"
    )

    return content

</code></pre>



<p>We now have a unique index on the three interactive components : the checkbox, the input and the delete button. Let’s write the callbacks.</p>



<h3 id='2-adding-new-tasks'  id="boomdevs_10" class="wp-block-heading" >2. Adding new tasks</h3>



<p>Now we can implement the &#8220;Add Task&#8221; functionality using callbacks:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("main_task_container", "children", allow_duplicate=True),
    Input("new_task_button", "n_clicks"),
    State("main_task_container", "children"),
    prevent_initial_call=True,
)
def add_task(n_clicks, current_tasks):
    """ Adds a task to the list """
    if not n_clicks:
        raise PreventUpdate

    # Create new task with unique ID
    new_index = uuid.uuid4().hex
    task_dict = {
        "index": new_index,
        "content": "",
        "checked": False,
    }
    task_layout = get_task(task_dict)

    # Add new task to current tasks
    updated_tasks = current_tasks + [task_layout]
    return updated_tasks

</code></pre>



<p>This callback is triggered with the new_task_button is clicked. It basically takes the existing list of tasks that are in <code>main_task_container</code> and add a new, empty, task inside. Then it returns the new list.</p>



<p>We added <code>prevent_initial_call=True</code> to avoid running this callback when the app is loaded (the default behavior), as we know that this callback should only be triggered on a button action. It reduces unnecessary work and HTTP requests.</p>



<p>We also check the <code>n_clicks</code> is a valid positive value, in case that the callback gets triggered anyway. The <code>raise PreventUpdate</code> stops the callback execution.</p>



<p class="callout"><strong>Notice</strong> the <code>allow_duplicate=True</code> on the <code>Output</code>. It is mandatory as we will have many operations that will update the <code>main_task_container</code> component.</p>



<h3 id='3-removing-tasks'  id="boomdevs_11" class="wp-block-heading" >3. Removing Tasks</h3>



<p>And finally, we implement the “Task deletion” callback following the same scheme:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">@app.callback(
    Output("main_task_container", "children", allow_duplicate=True),
    Input({"type": "task_del", "index": ALL}, "n_clicks"),
    State("main_task_container", "children"),
    prevent_initial_call=True,
)
def remove_task(n_clicks, current_tasks):
    """ Remove a task from the list """
    if not any(n_clicks):
        raise PreventUpdate

    print("Entering remove_task callback")
    task_index = ctx.triggered_id["index"]

    # Get the list of existing ids.
    all_ids = [elem["id"] for elem in ctx.inputs_list[0]]

    # Find the position of element in list and remove it
    for i, task_id in enumerate(all_ids):
        if task_id["index"] == task_index:
            del current_tasks[i]
            break 

    return current_tasks
</code></pre>



<p>Here&#8217;s how it works:</p>



<ol class="wp-block-list">
<li>When a delete button is clicked, Dash triggers this callback. We use pattern matching with <code>{"type": "task_del", "index": ALL}</code> to catch clicks from any delete button in our tasks. Each button has a unique index we assigned earlier.</li>



<li>The <code>ctx.triggered_id</code> tells us which specific delete button was clicked &#8211; specifically its index. This works because when the callback fires, Dash knows exactly which component triggered it.</li>



<li>To find and remove the correct task, we need to:
<ul class="wp-block-list">
<li>Get all delete button IDs using <code>ctx.inputs_list[0]</code> which contains the IDs of all components matching our pattern</li>



<li>Map this to just get the ID dictionaries (each with a &#8220;type&#8221; and &#8220;index&#8221;)</li>



<li>Find the position (index) in our task list that matches the triggered button&#8217;s index</li>



<li>Delete that task from <code>current_tasks</code> using <code>del</code></li>
</ul>
</li>
</ol>



<p>As seen previously, the <code>prevent_initial_call=True</code> and <code>if not any(n_clicks)</code> check ensure we don&#8217;t accidentally delete tasks when the app first loads or if the callback is triggered without a click.</p>



<h3 id='4-run-the-app'  id="boomdevs_12" class="wp-block-heading" >4. Run the app</h3>



<p>Let’s run again the code. We preferably place our callbacks before the <code>app.run_server</code> statement:</p>



<pre class="wp-block-code"><code lang="python" class="language-python">import uuid
import dash_mantine_components as dmc
from dash import Dash, _dash_renderer, dcc, ctx, Input, Output, State, ALL
from dash.exceptions import PreventUpdate
from dash_iconify import DashIconify
_dash_renderer._set_react_version("18.2.0")  # for mantine

# sample_list_data = ...

## Layout functions:
# get_task()
# get_tasks_layout()
# get_list_layout

app = Dash(__name__)

# app.layout = ..

## Callbacks
# add_task()
# remove_task()

# Start the server
if __name__ == '__main__':
    app.run_server(debug=True)
</code></pre>



<p>Try to add a task, modify and delete a task in the app below:</p>



<iframe loading="lazy" src="https://scripts.dash-resources.com/todo_app/app2.py/" width="100%" height="400" frameBorder="0"></iframe>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/27a1.png" alt="➡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/Spriteware/todo-app-python-dash/blob/master/app2.py">See the full code on Github</a> or <a href="https://scripts.dash-resources.com/todo_app/app2.py/">open the app</a>.</p>



<p>You can also download the full project below:</p>



                <div class="ml-embedded" data-form="LGDFB1"></div>
            



<p>You now have a fully working —yet basic— todo app. Congratulations!</p>



<h2 id='conclusion'  id="boomdevs_13" class="wp-block-heading" >Conclusion</h2>



<p>Let&#8217;s recap what we covered in this article:</p>



<ul class="wp-block-list">
<li>An introduction to DMC (Dash Mantine Components) and its components</li>



<li>How to use pattern matching callbacks for dynamic interactions</li>



<li>How to use CSS styling in a Dash context</li>
</ul>



<p>In the next part, we will see how to adapt this code to handle multiple lists and keep the tasks saved after page reloads (persistence): <a href="https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-2-3/">How to build a To-Do app, part 2</a>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>I hope you enjoyed this first part of the tutorial! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p>If you have any question, please join us on the dedicated topic on Plotly’s forum: <a href="https://community.plotly.com/t/tutorial-build-a-to-do-app-in-python-with-dash/89805">here</a>. <strong>Get notified of a new article by subscribing to the newsletter. </strong></p>
<p>L’article <a href="https://dash-resources.com/build-a-to-do-app-in-python-with-dash-part-1-3/">Build a To-Do app in Python with Dash (part 1/3)</a> est apparu en premier sur <a href="https://dash-resources.com">dash-resources.com</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
