[{"data":1,"prerenderedAt":255},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-python-sets-en":69},{},{"featuredPost":5,"navigations":26},{"id":6,"title":7,"title_en":8,"slug":9,"excerpt":10,"excerpt_en":8,"has_translation":11,"published_at":12,"reading_time":13,"hidden_from_listing":11,"tags":14,"thumbnail_image":8,"og_image":19,"series":20,"authors":25},110,"Ketika Pilih Framework Cuma Biar Kelihatan Beda",null,"ketika-pilih-framework-cuma-biar-kelihatan-beda","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.",false,"2026-10-06T15:25:21.000Z",5,[15,16,17,18],"karier","framework","opini","pengalaman","\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":21,"title":22,"title_en":23,"slug":24},9,"Artikel","Articles","all",[],[27,43,64],{"collection":28,"item":29},"series",{"__typename":28,"title":22,"title_en":23,"slug":24,"children_series":30},[31,35,39],{"title":32,"title_en":33,"slug":34},"Kecerdasan Artifisial","Artificial Intelligence","kecerdasan-artifisial",{"title":36,"title_en":37,"slug":38},"Konsep Dasar Pemrograman","Programming Fundamentals","konsep-dasar-pemrograman",{"title":40,"title_en":41,"slug":42},"Algoritma dan Struktur Data","Algorithms and Data Structures","algoritma-struktur-data",{"collection":28,"item":44},{"__typename":28,"title":45,"title_en":45,"slug":46,"children_series":47},"Tutorial","tutorial",[48,52,56,60],{"title":49,"title_en":50,"slug":51},"Belajar Python","Learning Python","belajar-python",{"title":53,"title_en":54,"slug":55},"Belajar Golang","Learning Go","belajar-golang",{"title":57,"title_en":58,"slug":59},"Belajar Java","Learning Java","belajar-java",{"title":61,"title_en":62,"slug":63},"Belajar Pascal","Learning Pascal","belajar-pascal",{"collection":28,"item":65},{"__typename":28,"title":66,"title_en":66,"slug":67,"children_series":68},"Lab","lab",[],{"article":70,"renderedBody":163,"renderedTakeaway":164,"latestArticles":165,"toc":235},{"id":71,"title":72,"title_en":8,"slug":73,"excerpt":74,"excerpt_en":8,"has_translation":11,"published_at":75,"reading_time":76,"hidden_from_listing":11,"tags":77,"thumbnail_image":8,"og_image":80,"series":81,"authors":155,"body_locale":158,"available_locales":159,"body":161,"takeway":162,"prerequisite":8},131,"Python Sets: the Duplicate That Keeps Coming Back","python-sets","A de-duplication loop that works on clean data leaves four tags where there should be two. The bug is case, and the fix is a `set`.","2026-10-28T11:00:00.000Z",6,[78,79],"python","set","\u002Fog\u002Fpython-sets.png",{"id":82,"title":83,"title_en":84,"slug":85,"description":86,"description_en":87,"articles":88,"children_series":124,"parent_series":125},30,"Koleksi dan String","Collections and Strings","python-collections","Dictionary, set, tuple, dan cara mengolah string. Tipe data yang baru kepakai begitu program berhenti menyimpan satu nilai saja.","Dictionaries, sets, tuples, and the string methods that do the boring parts. The types you need once a program stops holding a single value.",[89,106,111],{"id":90,"title":91,"title_en":8,"slug":92,"excerpt":93,"excerpt_en":8,"has_translation":11,"published_at":94,"reading_time":76,"hidden_from_listing":11,"tags":95,"thumbnail_image":8,"og_image":97,"series":98,"authors":99},130,"Python Dictionaries: the Key That Isn't There","python-dictionary","A word counter dies the first time it meets a word twice. The fix is one line; what takes longer is knowing which of a dict's three lookups to reach for.","2026-10-28T10:00:00.000Z",[78,96],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":82,"title":83,"title_en":84,"slug":85},[100],{"authors_id":101},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},1,"Daffa Izzuddin","izzudd","Writer",{"id":71,"title":72,"title_en":8,"slug":73,"excerpt":74,"excerpt_en":8,"has_translation":11,"published_at":75,"reading_time":76,"hidden_from_listing":11,"tags":77,"thumbnail_image":8,"og_image":80,"series":107,"authors":108},{"id":82,"title":83,"title_en":84,"slug":85},[109],{"authors_id":110},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},{"id":112,"title":113,"title_en":8,"slug":114,"excerpt":115,"excerpt_en":8,"has_translation":11,"published_at":116,"reading_time":76,"hidden_from_listing":11,"tags":117,"thumbnail_image":8,"og_image":119,"series":120,"authors":121},132,"Python Tuples: Why All Three Points Are the Same Point","python-tuples","Three points walk into a loop and come out identical. The list was never copied three times; the name was. A tuple makes that bug impossible.","2026-10-28T12:00:00.000Z",[78,118],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":82,"title":83,"title_en":84,"slug":85},[122],{"authors_id":123},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},[],{"id":126,"title":49,"title_en":50,"slug":51,"parent_series":127,"children_series":128},10,{"id":102,"title":45,"title_en":45,"slug":46},[129,134,139,144,149,154],{"id":130,"title":131,"title_en":132,"slug":133,"cover_image":8},16,"Berkenalan dengan Bahasa Python","Getting Started with Python","python-intro",{"id":135,"title":136,"title_en":137,"slug":138,"cover_image":8},17,"Variabel, Tipe Data, dan Operasi","Variables, Types, and Operations","python-variabel-tipe-data",{"id":140,"title":141,"title_en":142,"slug":143,"cover_image":8},18,"Menggunakan Script Python","Working with Python Scripts","python-pemrograman-dengan-script",{"id":145,"title":146,"title_en":147,"slug":148,"cover_image":8},19,"Struktur Kontrol","Control Structures","python-struktur-kontrol",{"id":150,"title":151,"title_en":152,"slug":153,"cover_image":8},20,"Pemrograman Fungsional dengan Python","Functional Programming with Python","python-pemrograman-fungsional",{"id":82,"title":83,"title_en":84,"slug":85,"cover_image":8},[156],{"authors_id":157},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},"en",[160],"id","\nA tag list came out of a spreadsheet. Five rows, four tags, two of them the same word typed differently.\n\nI deduplicated it with the loop everyone writes, and got four tags back. The loop was not wrong in any way I could see: it checked every tag against the ones already kept, and it never kept the same string twice.\n\n## Two files, one command\n\nSame contract as the rest of this series: you write the program, the article hands you a checker, you run it until it goes from red to green. No repository, no Docker, just `python3`.\n\nCreate `unique_tags.py`:\n\n```python\n# unique_tags.py\nimport sys\n\ntags = [line.strip() for line in sys.stdin if line.strip()]\n\nseen = []\nfor tag in tags:\n    if tag not in seen:\n        seen.append(tag)\n\nprint(\"\\n\".join(seen))\n```\n\nThen `check_unique_tags.py` next to it. It feeds in the spreadsheet rows and reads what comes out:\n\n```python\n# check_unique_tags.py\nimport subprocess\nimport sys\n\nPOST = \"\"\"python\nPython\nweb\nPYTHON\nweb\n\"\"\"\nEXPECTED = [\"python\", \"web\"]\n\n\ndef run_script():\n    return subprocess.run(\n        [sys.executable, \"unique_tags.py\"],\n        input=POST,\n        text=True,\n        capture_output=True,\n    )\n\n\ndef report(results):\n    for label, passed, detail in results:\n        mark = \"PASS\" if passed else \"FAIL\"\n        note = f\"  \u003C- {detail}\" if detail and not passed else \"\"\n        print(f\"{mark}  {label}{note}\")\n    passed = [ok for _, ok, _ in results].count(True)\n    print(f\"\\n{passed} of {len(results)} checks passed\")\n\n\ndef main():\n    run = run_script()\n    crashed = run.returncode != 0\n    last_error = run.stderr.strip().splitlines()[-1] if run.stderr.strip() else \"\"\n\n    results = [(\"the script runs without crashing\", not crashed, last_error)]\n    if crashed:\n        report(results)\n        return 1\n\n    lines = [line.strip() for line in run.stdout.splitlines() if line.strip()]\n    lowered = [line.lower() for line in lines]\n\n    results.append((\"prints one tag per line\", len(lines) == len(run.stdout.splitlines()), str(lines)))\n    results.append((\"leaves two tags\", len(lines) == len(EXPECTED), f\"got {len(lines)}: {lines}\"))\n    results.append((\"no tag appears twice\", len(set(lowered)) == len(lowered), str(lines)))\n    results.append((\"the tags are lowercase\", lines == lowered, str(lines)))\n    results.append((\"the first appearance order is kept\", lowered == EXPECTED, str(lines)))\n\n    report(results)\n    return 0 if all(ok for _, ok, _ in results) else 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```\n\n```\n$ python3 check_unique_tags.py\nPASS  the script runs without crashing\nPASS  prints one tag per line\nFAIL  leaves two tags  \u003C- got 4: ['python', 'Python', 'web', 'PYTHON']\nFAIL  no tag appears twice  \u003C- ['python', 'Python', 'web', 'PYTHON']\nFAIL  the tags are lowercase  \u003C- ['python', 'Python', 'web', 'PYTHON']\nFAIL  the first appearance order is kept  \u003C- ['python', 'Python', 'web', 'PYTHON']\n\n2 of 6 checks passed\n```\n\nFour failures, one cause.\n\n## `not in` is not \"looks the same\"\n\n`tag not in seen` asks one question: is this exact string already in the list. `Python` and `python` are two different strings to Python, the same way `2` and `\"2\"` are different values — the characters differ, so the comparison fails.\n\nWhich means the loop is only correct if the data is already normalised. Spreadsheet exports never are.\n\nThe fix has to happen **while you read**, not while you compare. Lower-case each tag before it enters the list, and the comparison problem disappears:\n\n```python\ntags = [line.strip().lower() for line in sys.stdin if line.strip()]\n```\n\n## A set answers \"is it there\" faster\n\nOnce the data is normalised, there is a simpler tool for the job. A `set` is a collection that cannot hold duplicates, so \"have I seen this tag\" becomes a single membership check instead of a walk through a list.\n\n```python\n# unique_tags.py\nimport sys\n\ntags = [line.strip().lower() for line in sys.stdin if line.strip()]\n\nseen = set()\nunique = []\nfor tag in tags:\n    if tag not in seen:\n        seen.add(tag)\n        unique.append(tag)\n\nprint(\"\\n\".join(unique))\n```\n\n```\n$ python3 check_unique_tags.py\nPASS  the script runs without crashing\nPASS  prints one tag per line\nPASS  leaves two tags\nPASS  no tag appears twice\nPASS  the tags are lowercase\nPASS  the first appearance order is kept\n\n6 of 6 checks passed\n```\n\nTwo containers, two jobs: the `set` answers \"seen or not\", the list keeps the order for the output. That is not a workaround for a language limitation, it is the normal division of labour.\n\n## The one-liner that loses the order\n\nYou will meet this soon, and it is worth knowing why it is a trap:\n\n```python\nprint(\"\\n\".join(set(tags)))\n```\n\nIt does remove the duplicates. It also throws away the order, because a set has none to keep. Run it four times on the same six tags and you get four different orders:\n\n```\n['sql', 'web', 'go', 'python', 'rust', 'ai']\n['go', 'rust', 'python', 'ai', 'web', 'sql']\n['web', 'rust', 'go', 'ai', 'python', 'sql']\n['rust', 'python', 'ai', 'sql', 'go', 'web']\n```\n\nIf the order matters to the reader, the set cannot be the thing you print. Use it to decide, then print from the list.\n\n## What a set is good for\n\nThe obvious use is membership: `if word in vocabulary` where `vocabulary` is a set, instead of walking a list every time you ask. The less obvious use is comparison, and it is the one people reinvent badly.\n\nFinding the tags two posts have in common is a nested loop by hand. With sets it is `a & b`. Tags that are new in the second post are `a - b`. Everything mentioned anywhere is `a | b`. Asking whether two posts overlap at all is `bool(a & b)`, and it is true or false without a loop. None of it needs your own bookkeeping, and none of it can be subtly wrong the way an inner loop can.\n\nThat last part deserves a sentence. A hand-written overlap loop has three ways to be wrong: the wrong comparison, a duplicate appended twice, and a case that looks fine because the test data happened to match. Set operations have none of them, because you did not write the loop.\n\nTwo things a set cannot do. You cannot index it, so `tags[0]` is an error rather than a surprise. And its members must be hashable, which is why a set of lists is not a thing — lists cannot go in a set for the same reason they cannot be dict keys.\n\nThe rule of thumb: **a list when order matters, a set when only membership does, a dict when each key carries a value.** When you need both order and uniqueness, that is two containers doing two jobs, not one clever line.\n\n## Before the next article\n\nRemove `.strip()` from the reading line, add one row with a trailing space (`\" web \"`) to `POST`, and run the check again. Five of six pass; the one that fails is about the shape of the output, not about duplicates. A line that looks fine in the terminal is not the same string as the tag without the space.\n\nPut `.strip()` back and it passes. That is the argument for normalising while you read, in one run.\n","- `if tag not in seen` compares exactly, so `Python` and `python` both survive a loop that looks correct.\n- Normalise while you read the data (`.lower()`, `.strip()`), not while you compare it.\n- `set(tags)` removes duplicates without keeping order: four runs of the same line gave four different orders.\n- Use a set when the question is \"is this in there\"; keep a list when the order is part of the answer.\n- Set members must be hashable, so a set of lists is not a thing.","\u003Cp>A tag list came out of a spreadsheet. Five rows, four tags, two of them the same word typed differently.\u003C\u002Fp>\n\u003Cp>I deduplicated it with the loop everyone writes, and got four tags back. The loop was not wrong in any way I could see: it checked every tag against the ones already kept, and it never kept the same string twice.\u003C\u002Fp>\n\u003Ch2 id=\"two-files-one-command\">Two files, one command\u003C\u002Fh2>\n\u003Cp>Same contract as the rest of this series: you write the program, the article hands you a checker, you run it until it goes from red to green. No repository, no Docker, just \u003Ccode>python3\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Create \u003Ccode>unique_tags.py\u003C\u002Fcode>:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%23%20unique_tags.py%0Aimport%20sys%0A%0Atags%20%3D%20%5Bline.strip()%20for%20line%20in%20sys.stdin%20if%20line.strip()%5D%0A%0Aseen%20%3D%20%5B%5D%0Afor%20tag%20in%20tags%3A%0A%20%20%20%20if%20tag%20not%20in%20seen%3A%0A%20%20%20%20%20%20%20%20seen.append(tag)%0A%0Aprint(%22%5Cn%22.join(seen))\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># unique_tags.py\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sys\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">tags \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [line.strip() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sys.stdin \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line.strip()]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">seen \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> []\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> tag \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> tags:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> tag \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">not\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> seen:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        seen.append(tag)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">.join(seen))\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Then \u003Ccode>check_unique_tags.py\u003C\u002Fcode> next to it. It feeds in the spreadsheet rows and reads what comes out:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%23%20check_unique_tags.py%0Aimport%20subprocess%0Aimport%20sys%0A%0APOST%20%3D%20%22%22%22python%0APython%0Aweb%0APYTHON%0Aweb%0A%22%22%22%0AEXPECTED%20%3D%20%5B%22python%22%2C%20%22web%22%5D%0A%0A%0Adef%20run_script()%3A%0A%20%20%20%20return%20subprocess.run(%0A%20%20%20%20%20%20%20%20%5Bsys.executable%2C%20%22unique_tags.py%22%5D%2C%0A%20%20%20%20%20%20%20%20input%3DPOST%2C%0A%20%20%20%20%20%20%20%20text%3DTrue%2C%0A%20%20%20%20%20%20%20%20capture_output%3DTrue%2C%0A%20%20%20%20)%0A%0A%0Adef%20report(results)%3A%0A%20%20%20%20for%20label%2C%20passed%2C%20detail%20in%20results%3A%0A%20%20%20%20%20%20%20%20mark%20%3D%20%22PASS%22%20if%20passed%20else%20%22FAIL%22%0A%20%20%20%20%20%20%20%20note%20%3D%20f%22%20%20%3C-%20%7Bdetail%7D%22%20if%20detail%20and%20not%20passed%20else%20%22%22%0A%20%20%20%20%20%20%20%20print(f%22%7Bmark%7D%20%20%7Blabel%7D%7Bnote%7D%22)%0A%20%20%20%20passed%20%3D%20%5Bok%20for%20_%2C%20ok%2C%20_%20in%20results%5D.count(True)%0A%20%20%20%20print(f%22%5Cn%7Bpassed%7D%20of%20%7Blen(results)%7D%20checks%20passed%22)%0A%0A%0Adef%20main()%3A%0A%20%20%20%20run%20%3D%20run_script()%0A%20%20%20%20crashed%20%3D%20run.returncode%20!%3D%200%0A%20%20%20%20last_error%20%3D%20run.stderr.strip().splitlines()%5B-1%5D%20if%20run.stderr.strip()%20else%20%22%22%0A%0A%20%20%20%20results%20%3D%20%5B(%22the%20script%20runs%20without%20crashing%22%2C%20not%20crashed%2C%20last_error)%5D%0A%20%20%20%20if%20crashed%3A%0A%20%20%20%20%20%20%20%20report(results)%0A%20%20%20%20%20%20%20%20return%201%0A%0A%20%20%20%20lines%20%3D%20%5Bline.strip()%20for%20line%20in%20run.stdout.splitlines()%20if%20line.strip()%5D%0A%20%20%20%20lowered%20%3D%20%5Bline.lower()%20for%20line%20in%20lines%5D%0A%0A%20%20%20%20results.append((%22prints%20one%20tag%20per%20line%22%2C%20len(lines)%20%3D%3D%20len(run.stdout.splitlines())%2C%20str(lines)))%0A%20%20%20%20results.append((%22leaves%20two%20tags%22%2C%20len(lines)%20%3D%3D%20len(EXPECTED)%2C%20f%22got%20%7Blen(lines)%7D%3A%20%7Blines%7D%22))%0A%20%20%20%20results.append((%22no%20tag%20appears%20twice%22%2C%20len(set(lowered))%20%3D%3D%20len(lowered)%2C%20str(lines)))%0A%20%20%20%20results.append((%22the%20tags%20are%20lowercase%22%2C%20lines%20%3D%3D%20lowered%2C%20str(lines)))%0A%20%20%20%20results.append((%22the%20first%20appearance%20order%20is%20kept%22%2C%20lowered%20%3D%3D%20EXPECTED%2C%20str(lines)))%0A%0A%20%20%20%20report(results)%0A%20%20%20%20return%200%20if%20all(ok%20for%20_%2C%20ok%2C%20_%20in%20results)%20else%201%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20sys.exit(main())\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># check_unique_tags.py\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> subprocess\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sys\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">POST\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"\"\"python\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">Python\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">web\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">PYTHON\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">web\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">\"\"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">EXPECTED\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"python\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"web\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> run_script\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">():\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> subprocess.run(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        [sys.executable, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"unique_tags.py\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        input\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">POST\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        text\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        capture_output\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> report\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(results):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> label, passed, detail \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> results:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        mark \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"PASS\"\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> passed \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"FAIL\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        note \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"  &#x3C;- \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">detail\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> detail \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">and\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> not\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> passed \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">        print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">mark\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">  {\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">label\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">note\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    passed \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [ok \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> _, ok, _ \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> results].count(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">\\n{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">passed\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> of \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(results)\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> checks passed\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> main\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">():\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    run \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> run_script()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    crashed \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> run.returncode \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">!=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    last_error \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> run.stderr.strip().splitlines()[\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> run.stderr.strip() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the script runs without crashing\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">not\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> crashed, last_error)]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> crashed:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        report(results)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    lines \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [line.strip() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> run.stdout.splitlines() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line.strip()]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    lowered \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [line.lower() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> lines]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"prints one tag per line\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lines) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(run.stdout.splitlines()), \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lines)))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"leaves two tags\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lines) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">EXPECTED\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">), \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"got \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lines)\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">lines\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"no tag appears twice\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">set\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lowered)) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lowered), \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lines)))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the tags are lowercase\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, lines \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> lowered, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lines)))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"the first appearance order is kept\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, lowered \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> EXPECTED\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">str\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(lines)))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    report(results)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> all\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(ok \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> _, ok, _ \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> results) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> __name__\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> ==\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"__main__\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    sys.exit(main())\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20check_unique_tags.py%0APASS%20%20the%20script%20runs%20without%20crashing%0APASS%20%20prints%20one%20tag%20per%20line%0AFAIL%20%20leaves%20two%20tags%20%20%3C-%20got%204%3A%20%5B'python'%2C%20'Python'%2C%20'web'%2C%20'PYTHON'%5D%0AFAIL%20%20no%20tag%20appears%20twice%20%20%3C-%20%5B'python'%2C%20'Python'%2C%20'web'%2C%20'PYTHON'%5D%0AFAIL%20%20the%20tags%20are%20lowercase%20%20%3C-%20%5B'python'%2C%20'Python'%2C%20'web'%2C%20'PYTHON'%5D%0AFAIL%20%20the%20first%20appearance%20order%20is%20kept%20%20%3C-%20%5B'python'%2C%20'Python'%2C%20'web'%2C%20'PYTHON'%5D%0A%0A2%20of%206%20checks%20passed\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>$ python3 check_unique_tags.py\nPASS  the script runs without crashing\nPASS  prints one tag per line\nFAIL  leaves two tags  &lt;- got 4: ['python', 'Python', 'web', 'PYTHON']\nFAIL  no tag appears twice  &lt;- ['python', 'Python', 'web', 'PYTHON']\nFAIL  the tags are lowercase  &lt;- ['python', 'Python', 'web', 'PYTHON']\nFAIL  the first appearance order is kept  &lt;- ['python', 'Python', 'web', 'PYTHON']\n\n2 of 6 checks passed\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Four failures, one cause.\u003C\u002Fp>\n\u003Ch2 id=\"not-in-is-not-looks-the-same\">\u003Ccode>not in\u003C\u002Fcode> is not &quot;looks the same&quot;\u003C\u002Fh2>\n\u003Cp>\u003Ccode>tag not in seen\u003C\u002Fcode> asks one question: is this exact string already in the list. \u003Ccode>Python\u003C\u002Fcode> and \u003Ccode>python\u003C\u002Fcode> are two different strings to Python, the same way \u003Ccode>2\u003C\u002Fcode> and \u003Ccode>&quot;2&quot;\u003C\u002Fcode> are different values — the characters differ, so the comparison fails.\u003C\u002Fp>\n\u003Cp>Which means the loop is only correct if the data is already normalised. Spreadsheet exports never are.\u003C\u002Fp>\n\u003Cp>The fix has to happen \u003Cstrong>while you read\u003C\u002Fstrong>, not while you compare. Lower-case each tag before it enters the list, and the comparison problem disappears:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"tags%20%3D%20%5Bline.strip().lower()%20for%20line%20in%20sys.stdin%20if%20line.strip()%5D\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">tags \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [line.strip().lower() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sys.stdin \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line.strip()]\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Ch2 id=\"a-set-answers-is-it-there-faster\">A set answers &quot;is it there&quot; faster\u003C\u002Fh2>\n\u003Cp>Once the data is normalised, there is a simpler tool for the job. A \u003Ccode>set\u003C\u002Fcode> is a collection that cannot hold duplicates, so &quot;have I seen this tag&quot; becomes a single membership check instead of a walk through a list.\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%23%20unique_tags.py%0Aimport%20sys%0A%0Atags%20%3D%20%5Bline.strip().lower()%20for%20line%20in%20sys.stdin%20if%20line.strip()%5D%0A%0Aseen%20%3D%20set()%0Aunique%20%3D%20%5B%5D%0Afor%20tag%20in%20tags%3A%0A%20%20%20%20if%20tag%20not%20in%20seen%3A%0A%20%20%20%20%20%20%20%20seen.add(tag)%0A%20%20%20%20%20%20%20%20unique.append(tag)%0A%0Aprint(%22%5Cn%22.join(unique))\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># unique_tags.py\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sys\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">tags \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [line.strip().lower() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sys.stdin \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> line.strip()]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">seen \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> set\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">unique \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> []\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> tag \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> tags:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> tag \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">not\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> seen:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        seen.add(tag)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        unique.append(tag)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">.join(unique))\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20check_unique_tags.py%0APASS%20%20the%20script%20runs%20without%20crashing%0APASS%20%20prints%20one%20tag%20per%20line%0APASS%20%20leaves%20two%20tags%0APASS%20%20no%20tag%20appears%20twice%0APASS%20%20the%20tags%20are%20lowercase%0APASS%20%20the%20first%20appearance%20order%20is%20kept%0A%0A6%20of%206%20checks%20passed\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>$ python3 check_unique_tags.py\nPASS  the script runs without crashing\nPASS  prints one tag per line\nPASS  leaves two tags\nPASS  no tag appears twice\nPASS  the tags are lowercase\nPASS  the first appearance order is kept\n\n6 of 6 checks passed\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Two containers, two jobs: the \u003Ccode>set\u003C\u002Fcode> answers &quot;seen or not&quot;, the list keeps the order for the output. That is not a workaround for a language limitation, it is the normal division of labour.\u003C\u002Fp>\n\u003Ch2 id=\"the-one-liner-that-loses-the-order\">The one-liner that loses the order\u003C\u002Fh2>\n\u003Cp>You will meet this soon, and it is worth knowing why it is a trap:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"print(%22%5Cn%22.join(set(tags)))\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">.join(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">set\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(tags)))\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>It does remove the duplicates. It also throws away the order, because a set has none to keep. Run it four times on the same six tags and you get four different orders:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%5B'sql'%2C%20'web'%2C%20'go'%2C%20'python'%2C%20'rust'%2C%20'ai'%5D%0A%5B'go'%2C%20'rust'%2C%20'python'%2C%20'ai'%2C%20'web'%2C%20'sql'%5D%0A%5B'web'%2C%20'rust'%2C%20'go'%2C%20'ai'%2C%20'python'%2C%20'sql'%5D%0A%5B'rust'%2C%20'python'%2C%20'ai'%2C%20'sql'%2C%20'go'%2C%20'web'%5D\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>['sql', 'web', 'go', 'python', 'rust', 'ai']\n['go', 'rust', 'python', 'ai', 'web', 'sql']\n['web', 'rust', 'go', 'ai', 'python', 'sql']\n['rust', 'python', 'ai', 'sql', 'go', 'web']\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>If the order matters to the reader, the set cannot be the thing you print. Use it to decide, then print from the list.\u003C\u002Fp>\n\u003Ch2 id=\"what-a-set-is-good-for\">What a set is good for\u003C\u002Fh2>\n\u003Cp>The obvious use is membership: \u003Ccode>if word in vocabulary\u003C\u002Fcode> where \u003Ccode>vocabulary\u003C\u002Fcode> is a set, instead of walking a list every time you ask. The less obvious use is comparison, and it is the one people reinvent badly.\u003C\u002Fp>\n\u003Cp>Finding the tags two posts have in common is a nested loop by hand. With sets it is \u003Ccode>a &amp; b\u003C\u002Fcode>. Tags that are new in the second post are \u003Ccode>a - b\u003C\u002Fcode>. Everything mentioned anywhere is \u003Ccode>a | b\u003C\u002Fcode>. Asking whether two posts overlap at all is \u003Ccode>bool(a &amp; b)\u003C\u002Fcode>, and it is true or false without a loop. None of it needs your own bookkeeping, and none of it can be subtly wrong the way an inner loop can.\u003C\u002Fp>\n\u003Cp>That last part deserves a sentence. A hand-written overlap loop has three ways to be wrong: the wrong comparison, a duplicate appended twice, and a case that looks fine because the test data happened to match. Set operations have none of them, because you did not write the loop.\u003C\u002Fp>\n\u003Cp>Two things a set cannot do. You cannot index it, so \u003Ccode>tags[0]\u003C\u002Fcode> is an error rather than a surprise. And its members must be hashable, which is why a set of lists is not a thing — lists cannot go in a set for the same reason they cannot be dict keys.\u003C\u002Fp>\n\u003Cp>The rule of thumb: \u003Cstrong>a list when order matters, a set when only membership does, a dict when each key carries a value.\u003C\u002Fstrong> When you need both order and uniqueness, that is two containers doing two jobs, not one clever line.\u003C\u002Fp>\n\u003Ch2 id=\"before-the-next-article\">Before the next article\u003C\u002Fh2>\n\u003Cp>Remove \u003Ccode>.strip()\u003C\u002Fcode> from the reading line, add one row with a trailing space (\u003Ccode>&quot; web &quot;\u003C\u002Fcode>) to \u003Ccode>POST\u003C\u002Fcode>, and run the check again. Five of six pass; the one that fails is about the shape of the output, not about duplicates. A line that looks fine in the terminal is not the same string as the tag without the space.\u003C\u002Fp>\n\u003Cp>Put \u003Ccode>.strip()\u003C\u002Fcode> back and it passes. That is the argument for normalising while you read, in one run.\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>\u003Ccode>if tag not in seen\u003C\u002Fcode> compares exactly, so \u003Ccode>Python\u003C\u002Fcode> and \u003Ccode>python\u003C\u002Fcode> both survive a loop that looks correct.\u003C\u002Fli>\n\u003Cli>Normalise while you read the data (\u003Ccode>.lower()\u003C\u002Fcode>, \u003Ccode>.strip()\u003C\u002Fcode>), not while you compare it.\u003C\u002Fli>\n\u003Cli>\u003Ccode>set(tags)\u003C\u002Fcode> removes duplicates without keeping order: four runs of the same line gave four different orders.\u003C\u002Fli>\n\u003Cli>Use a set when the question is &quot;is this in there&quot;; keep a list when the order is part of the answer.\u003C\u002Fli>\n\u003Cli>Set members must be hashable, so a set of lists is not a thing.\u003C\u002Fli>\n\u003C\u002Ful>\n",[166,171,176,181,200,218],{"id":112,"title":113,"title_en":8,"slug":114,"excerpt":115,"excerpt_en":8,"has_translation":11,"published_at":116,"reading_time":76,"hidden_from_listing":11,"tags":117,"thumbnail_image":8,"og_image":119,"series":167,"authors":168},{"id":82,"title":83,"title_en":84,"slug":85},[169],{"authors_id":170},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},{"id":71,"title":72,"title_en":8,"slug":73,"excerpt":74,"excerpt_en":8,"has_translation":11,"published_at":75,"reading_time":76,"hidden_from_listing":11,"tags":77,"thumbnail_image":8,"og_image":80,"series":172,"authors":173},{"id":82,"title":83,"title_en":84,"slug":85},[174],{"authors_id":175},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},{"id":90,"title":91,"title_en":8,"slug":92,"excerpt":93,"excerpt_en":8,"has_translation":11,"published_at":94,"reading_time":76,"hidden_from_listing":11,"tags":95,"thumbnail_image":8,"og_image":97,"series":177,"authors":178},{"id":82,"title":83,"title_en":84,"slug":85},[179],{"authors_id":180},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},{"id":182,"title":183,"title_en":8,"slug":184,"excerpt":185,"excerpt_en":8,"has_translation":11,"published_at":186,"reading_time":13,"hidden_from_listing":11,"tags":187,"thumbnail_image":8,"og_image":194,"series":195,"authors":197},129,"Biar RAG nggak kena 504: pisahin jalur query dan ingestion","rag-pisah-jalur-query-dan-ingestion","Satu service RAG yang merangkap parsing dan menjawab kueri bakal tumbang pas ada yang upload PDF 200 halaman. Obatnya dua jalur dan satu queue.","2026-10-22T01:30:00.000Z",[188,189,190,191,192,193],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":196,"title":32,"title_en":33,"slug":34},8,[198],{"authors_id":199},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},{"id":201,"title":202,"title_en":8,"slug":203,"excerpt":204,"excerpt_en":8,"has_translation":11,"published_at":205,"reading_time":76,"hidden_from_listing":11,"tags":206,"thumbnail_image":8,"og_image":213,"series":214,"authors":215},128,"Linear atau graf: cara mengatur banyak agent tanpa loop abadi","multi-agen-linear-atau-grafik","Satu agent dengan dua puluh tool gampang kacau. Begitu beberapa agent saling revisi, kamu butuh graf yang mendukung cycle dan angka yang membuatnya berhenti.","2026-10-21T16:05:00.000Z",[207,208,209,210,211,212],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":196,"title":32,"title_en":33,"slug":34},[216],{"authors_id":217},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},{"id":219,"title":220,"title_en":8,"slug":221,"excerpt":222,"excerpt_en":8,"has_translation":11,"published_at":223,"reading_time":76,"hidden_from_listing":11,"tags":224,"thumbnail_image":8,"og_image":230,"series":231,"authors":232},127,"Kenapa RAG yang cuma andalkan cosine similarity sering nyasar","kenapa-rag-cuma-cosine-similarity-sering-nyasar","RAG yang cuma andalkan embedding balikin chunk yang mirip maknanya, bukan yang kamu butuh. Ini penyebabnya, dan dua lapis penambalnya.","2026-10-19T02:04:06.718Z",[188,225,226,227,228,229],"vector-search","bm25","hybrid-search","retrieval","embedding","\u002Fog\u002Fkenapa-rag-cuma-cosine-similarity-sering-nyasar.png",{"id":196,"title":32,"title_en":33,"slug":34},[233],{"authors_id":234},{"id":102,"name":103,"slug":104,"role":105,"profile_picture":8},[236,240,243,246,249,252],{"text":237,"depth":238,"id":239},"Two files, one command",2,"two-files-one-command",{"text":241,"depth":238,"id":242},"not in is not \"looks the same\"","not-in-is-not-looks-the-same",{"text":244,"depth":238,"id":245},"A set answers \"is it there\" faster","a-set-answers-is-it-there-faster",{"text":247,"depth":238,"id":248},"The one-liner that loses the order","the-one-liner-that-loses-the-order",{"text":250,"depth":238,"id":251},"What a set is good for","what-a-set-is-good-for",{"text":253,"depth":238,"id":254},"Before the next article","before-the-next-article",1793246466870]