{"course":{"slug":"hugging-face","title":"Hugging Face, End to End","url":"https://addaly.com/learn/hugging-face","level":"intermediate","track":"create"},"lesson":{"slug":"gradio-blocks-and-state","title":"Blocks, when Interface is no longer enough","url":"https://addaly.com/learn/hugging-face/gradio-blocks-and-state","position":30,"of":75,"minutes":9,"answer":"gr.Blocks separates layout from event wiring, and the rule that prevents the worst class of Space bug is that module-level variables are shared by every visitor while gr.State is per-session.","keyIdea":"`gr.Blocks` separates layout from event wiring, and the rule that prevents the worst class of Space bug is that module-level variables are shared by every visitor while `gr.State` is per-session, so the model is shared and everything about a user is not."},"body":{"format":"markdown","text":"## When to move\n\n`gr.Interface` is one function, one form. Move to `gr.Blocks` when you need layout, several buttons doing different things, outputs that feed back into inputs, or anything conversational.\n\n```python\nimport gradio as gr\n\nwith gr.Blocks(title=\"Transcribe and summarise\") as demo:\n    gr.Markdown(\"## Upload audio, get a summary\")\n\n    with gr.Row():\n        with gr.Column(scale=1):\n            audio = gr.Audio(type=\"filepath\", label=\"Audio\")\n            lang = gr.Dropdown([\"auto\", \"en\", \"hi\"], value=\"auto\", label=\"Language\")\n            go = gr.Button(\"Transcribe\", variant=\"primary\")\n        with gr.Column(scale=2):\n            text = gr.Textbox(label=\"Transcript\", lines=10)\n            summary = gr.Textbox(label=\"Summary\", lines=4)\n            sum_btn = gr.Button(\"Summarise this\")\n\n    go.click(fn=transcribe, inputs=[audio, lang], outputs=text)\n    sum_btn.click(fn=summarise, inputs=text, outputs=summary)\n\ndemo.queue().launch()\n```\n\nThe pattern is always the same: declare components inside the `with` block, then wire events with `component.click(fn=..., inputs=..., outputs=...)`. `inputs` and `outputs` are lists of components, and Gradio passes their current values in and writes returned values back.\n\n## Chat, which is a component\n\n```python\ndef respond(message, history):\n    messages = history + [{\"role\": \"user\", \"content\": message}]\n    out = pipe(messages, max_new_tokens=256)\n    return out[0][\"generated_text\"][-1][\"content\"]\n\ngr.ChatInterface(respond, type=\"messages\").launch()\n```\n\n`gr.ChatInterface` gives you the message list, the history, retry, undo and clear. `history` arrives in exactly the `{\"role\", \"content\"}` shape that `apply_chat_template` wants, which is not a coincidence and saves a translation layer.\n\nFor streaming, make the function a generator and yield the accumulating string:\n\n```python\ndef respond(message, history):\n    partial = \"\"\n    for chunk in stream_from_model(message, history):\n        partial += chunk\n        yield partial\n```\n\nYield the whole string so far, not the chunk. Yielding chunks replaces the display each time and the user sees one word flickering.\n\n## State, and the mistake that leaks data between users\n\nThis is the most important paragraph in the lesson.\n\nA Space is one process serving everybody. A module-level variable is **shared by every visitor**:\n\n```python\nhistory = []              # WRONG: one list for the whole internet\n\ndef chat(msg):\n    history.append(msg)   # every user appends to the same list\n```\n\nOne person's conversation appears in another's. On anything handling personal text this is a data breach with a two-line cause, and it is easy to write because it works perfectly when you are the only user.\n\nThe fix is `gr.State`, which is per-session:\n\n```python\nwith gr.Blocks() as demo:\n    session = gr.State([])          # separate list per browser session\n    box = gr.Textbox()\n    out = gr.Textbox()\n\n    def chat(msg, hist):\n        hist = hist + [msg]\n        return \"\\n\".join(hist), hist\n\n    box.submit(chat, [box, session], [out, session])\n```\n\nThe state goes in as an input and comes back as an output. It lives as long as the browser session and dies with it. `gr.BrowserState` persists in local storage across reloads for preferences; anything that must outlive the session belongs in the storage lesson later in this module.\n\nShared module-level objects are correct for exactly one thing: the model, which is read-only and should be shared.\n\n## Making the interface respond to itself\n\n```python\nmode = gr.Radio([\"summarise\", \"translate\"], value=\"summarise\")\ntarget = gr.Dropdown([\"hi\", \"bn\", \"ta\"], visible=False)\n\nmode.change(\n    fn=lambda m: gr.update(visible=(m == \"translate\")),\n    inputs=mode, outputs=target,\n)\n```\n\n`gr.update()` changes a component's properties rather than its value: visibility, label, choices, interactivity. This is how a form stops showing fields that do not apply, which matters more for comprehension than it looks.\n\n## Progress, so the wait is legible\n\nA ten-second handler with no feedback reads as broken. Gradio will show a spinner on its own, and you can do better in four lines:\n\n```python\ndef transcribe(path, progress=gr.Progress()):\n    chunks = split_audio(path)\n    out = []\n    for c in progress.tqdm(chunks, desc=\"Transcribing\"):\n        out.append(asr(c)[\"text\"])\n    return \" \".join(out)\n```\n\nAdd the `progress` parameter and Gradio wires a bar into the interface. On free hardware, where everything is slower than you would like, this is the difference between a visitor waiting and a visitor reloading, and reloading queues a second job while the first is still running, which makes the problem worse for everybody.\n\n## The queue, briefly\n\n`demo.queue()` puts requests in a managed queue with position and estimated wait shown to the user. It is on by default in current versions and worth knowing about because `concurrency_limit` is how you stop eight simultaneous visitors from exhausting the memory of a free container. A queue that makes people wait visibly is better than a Space that crashes.\n\n## Do this now\n\nTake a Space you have built with `Interface` and rewrite it in `Blocks` with two buttons, where the second acts on the output of the first. Then add a `gr.State` and check that opening the Space in two browsers keeps the two sessions apart."},"check":{"question":"A chat Space keeps conversation history in a module-level list. It works perfectly in testing and, once public, users report seeing fragments of other people's conversations. Why?","options":[{"text":"Gradio's queue reuses request objects across sessions, so history written by one request is visible to the next.","correct":false,"tempting":"Blames the queue for what is ordinary Python scope; the queue schedules calls without sharing local data."},{"text":"The Space caches responses by input hash, so identical opening messages return another user's continuation.","correct":false,"tempting":"Invents a response cache that Gradio does not apply by default."},{"text":"Browser sessions share cookies on the huggingface.co domain, so session identifiers collide between visitors.","correct":false,"tempting":"Attributes to cookie scope a leak that exists entirely in server-side process state."},{"text":"One process serves every visitor, so a module-level list is a single object shared across all of them.","correct":true}]},"prev":{"slug":"gradio-in-forty-lines","title":"Forty lines from nothing to a public URL","url":"https://addaly.com/learn/hugging-face/gradio-in-forty-lines"},"next":{"slug":"duplicate-a-space","title":"Duplicate it, and the app becomes yours to change","url":"https://addaly.com/learn/hugging-face/duplicate-a-space"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Blocks, when Interface is no longer enough', in Hugging Face, End to End. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/hugging-face/gradio-blocks-and-state (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Blocks, when Interface is no longer enough","courseTitle":"Hugging Face, End to End","module":{"index":4,"slug":"spaces-and-shipping-something","title":"Spaces: putting something in front of people"},"answer":null,"keyIdea":"`gr.Blocks` separates layout from event wiring, and the rule that prevents the worst class of Space bug is that module-level variables are shared by every visitor while `gr.State` is per-session, so the model is shared and everything about a user is not.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"when-to-move","runs":[{"text":"When to move"}]},{"t":"p","runs":[{"text":"gr.Interface","code":true},{"text":" is one function, one form. Move to "},{"text":"gr.Blocks","code":true},{"text":" when you need layout, several buttons doing different things, outputs that feed back into inputs, or anything conversational."}]},{"t":"code","lang":"python","label":"Python","text":"import gradio as gr\n\nwith gr.Blocks(title=\"Transcribe and summarise\") as demo:\n    gr.Markdown(\"## Upload audio, get a summary\")\n\n    with gr.Row():\n        with gr.Column(scale=1):\n            audio = gr.Audio(type=\"filepath\", label=\"Audio\")\n            lang = gr.Dropdown([\"auto\", \"en\", \"hi\"], value=\"auto\", label=\"Language\")\n            go = gr.Button(\"Transcribe\", variant=\"primary\")\n        with gr.Column(scale=2):\n            text = gr.Textbox(label=\"Transcript\", lines=10)\n            summary = gr.Textbox(label=\"Summary\", lines=4)\n            sum_btn = gr.Button(\"Summarise this\")\n\n    go.click(fn=transcribe, inputs=[audio, lang], outputs=text)\n    sum_btn.click(fn=summarise, inputs=text, outputs=summary)\n\ndemo.queue().launch()"},{"t":"p","runs":[{"text":"The pattern is always the same: declare components inside the "},{"text":"with","code":true},{"text":" block, then wire events with "},{"text":"component.click(fn=..., inputs=..., outputs=...)","code":true},{"text":". "},{"text":"inputs","code":true},{"text":" and "},{"text":"outputs","code":true},{"text":" are lists of components, and Gradio passes their current values in and writes returned values back."}]},{"t":"h2","id":"chat-which-is-a-component","runs":[{"text":"Chat, which is a component"}]},{"t":"code","lang":"python","label":"Python","text":"def respond(message, history):\n    messages = history + [{\"role\": \"user\", \"content\": message}]\n    out = pipe(messages, max_new_tokens=256)\n    return out[0][\"generated_text\"][-1][\"content\"]\n\ngr.ChatInterface(respond, type=\"messages\").launch()"},{"t":"p","runs":[{"text":"gr.ChatInterface","code":true},{"text":" gives you the message list, the history, retry, undo and clear. "},{"text":"history","code":true},{"text":" arrives in exactly the "},{"text":"{\"role\", \"content\"}","code":true},{"text":" shape that "},{"text":"apply_chat_template","code":true},{"text":" wants, which is not a coincidence and saves a translation layer."}]},{"t":"p","runs":[{"text":"For streaming, make the function a generator and yield the accumulating string:"}]},{"t":"code","lang":"python","label":"Python","text":"def respond(message, history):\n    partial = \"\"\n    for chunk in stream_from_model(message, history):\n        partial += chunk\n        yield partial"},{"t":"p","runs":[{"text":"Yield the whole string so far, not the chunk. Yielding chunks replaces the display each time and the user sees one word flickering."}]},{"t":"h2","id":"state-and-the-mistake-that-leaks-data-between-users","runs":[{"text":"State, and the mistake that leaks data between users"}]},{"t":"p","runs":[{"text":"This is the most important paragraph in the lesson."}]},{"t":"p","runs":[{"text":"A Space is one process serving everybody. A module-level variable is "},{"text":"shared by every visitor","b":true},{"text":":"}]},{"t":"code","lang":"python","label":"Python","text":"history = []              # WRONG: one list for the whole internet\n\ndef chat(msg):\n    history.append(msg)   # every user appends to the same list"},{"t":"p","runs":[{"text":"One person’s conversation appears in another’s. On anything handling personal text this is a data breach with a two-line cause, and it is easy to write because it works perfectly when you are the only user."}]},{"t":"p","runs":[{"text":"The fix is "},{"text":"gr.State","code":true},{"text":", which is per-session:"}]},{"t":"code","lang":"python","label":"Python","text":"with gr.Blocks() as demo:\n    session = gr.State([])          # separate list per browser session\n    box = gr.Textbox()\n    out = gr.Textbox()\n\n    def chat(msg, hist):\n        hist = hist + [msg]\n        return \"\\n\".join(hist), hist\n\n    box.submit(chat, [box, session], [out, session])"},{"t":"p","runs":[{"text":"The state goes in as an input and comes back as an output. It lives as long as the browser session and dies with it. "},{"text":"gr.BrowserState","code":true},{"text":" persists in local storage across reloads for preferences; anything that must outlive the session belongs in the storage lesson later in this module."}]},{"t":"p","runs":[{"text":"Shared module-level objects are correct for exactly one thing: the model, which is read-only and should be shared."}]},{"t":"h2","id":"making-the-interface-respond-to-itself","runs":[{"text":"Making the interface respond to itself"}]},{"t":"code","lang":"python","label":"Python","text":"mode = gr.Radio([\"summarise\", \"translate\"], value=\"summarise\")\ntarget = gr.Dropdown([\"hi\", \"bn\", \"ta\"], visible=False)\n\nmode.change(\n    fn=lambda m: gr.update(visible=(m == \"translate\")),\n    inputs=mode, outputs=target,\n)"},{"t":"p","runs":[{"text":"gr.update()","code":true},{"text":" changes a component’s properties rather than its value: visibility, label, choices, interactivity. This is how a form stops showing fields that do not apply, which matters more for comprehension than it looks."}]},{"t":"h2","id":"progress-so-the-wait-is-legible","runs":[{"text":"Progress, so the wait is legible"}]},{"t":"p","runs":[{"text":"A ten-second handler with no feedback reads as broken. Gradio will show a spinner on its own, and you can do better in four lines:"}]},{"t":"code","lang":"python","label":"Python","text":"def transcribe(path, progress=gr.Progress()):\n    chunks = split_audio(path)\n    out = []\n    for c in progress.tqdm(chunks, desc=\"Transcribing\"):\n        out.append(asr(c)[\"text\"])\n    return \" \".join(out)"},{"t":"p","runs":[{"text":"Add the "},{"text":"progress","code":true},{"text":" parameter and Gradio wires a bar into the interface. On free hardware, where everything is slower than you would like, this is the difference between a visitor waiting and a visitor reloading, and reloading queues a second job while the first is still running, which makes the problem worse for everybody."}]},{"t":"h2","id":"the-queue-briefly","runs":[{"text":"The queue, briefly"}]},{"t":"p","runs":[{"text":"demo.queue()","code":true},{"text":" puts requests in a managed queue with position and estimated wait shown to the user. It is on by default in current versions and worth knowing about because "},{"text":"concurrency_limit","code":true},{"text":" is how you stop eight simultaneous visitors from exhausting the memory of a free container. A queue that makes people wait visibly is better than a Space that crashes."}]},{"t":"h2","id":"do-this-now","runs":[{"text":"Do this now"}],"role":"try"},{"t":"p","runs":[{"text":"Take a Space you have built with "},{"text":"Interface","code":true},{"text":" and rewrite it in "},{"text":"Blocks","code":true},{"text":" with two buttons, where the second acts on the output of the first. Then add a "},{"text":"gr.State","code":true},{"text":" and check that opening the Space in two browsers keeps the two sessions apart."}]}],"check":{"question":[{"text":"A chat Space keeps conversation history in a module-level list. It works perfectly in testing and, once public, users report seeing fragments of other people’s conversations. Why?"}],"options":[{"text":[{"text":"Gradio’s queue reuses request objects across sessions, so history written by one request is visible to the next."}],"correct":false,"tempting":[{"text":"Blames the queue for what is ordinary Python scope; the queue schedules calls without sharing local data."}]},{"text":[{"text":"The Space caches responses by input hash, so identical opening messages return another user’s continuation."}],"correct":false,"tempting":[{"text":"Invents a response cache that Gradio does not apply by default."}]},{"text":[{"text":"Browser sessions share cookies on the huggingface.co domain, so session identifiers collide between visitors."}],"correct":false,"tempting":[{"text":"Attributes to cookie scope a leak that exists entirely in server-side process state."}]},{"text":[{"text":"One process serves every visitor, so a module-level list is a single object shared across all of them."}],"correct":true,"tempting":null}],"order":[2,1,0,3],"why":null},"prev":{"slug":"gradio-in-forty-lines","title":"Forty lines from nothing to a public URL","minutes":9},"next":{"slug":"duplicate-a-space","title":"Duplicate it, and the app becomes yours to change","minutes":9}}}