{"record":{"id":"44306c07894e48bf","repo":"Textualize/textual","slug":"new-options-contain-duplicated-ids-ensure-that-th","errorCode":null,"errorMessage":"New options contain duplicated IDs; Ensure that the IDs are unique.","messagePattern":"New options contain duplicated IDs; Ensure that the IDs are unique\\.","errorType":"exception","errorClass":"DuplicateID","httpStatus":null,"severity":"error","filePath":"src/textual/widgets/_option_list.py","lineNumber":380,"sourceCode":"    def add_options(self, new_options: Iterable[OptionListContent]) -> Self:\n        \"\"\"Add new options.\n\n        Args:\n            new_options: Content of new options.\n\n        Returns:\n            The `OptionList` instance.\n        \"\"\"\n\n        new_options = list(new_options)\n\n        option_ids = [\n            option._id\n            for option in new_options\n            if isinstance(option, Option) and option._id is not None\n        ]\n        if len(option_ids) != len(set(option_ids)):\n            raise DuplicateID(\n                \"New options contain duplicated IDs; Ensure that the IDs are unique.\"\n            )\n\n        if not new_options:\n            return self\n        if new_options[0] is None:\n            # Handle the case where the first new option is None,\n            # which would update the previous option.\n            # This is sub-optimal, but hopefully not a common occurrence\n            self._clear_caches()\n        options = self._options\n        add_option = self._options.append\n\n        for prompt in new_options:\n            if isinstance(prompt, Option):\n                option = prompt\n            elif prompt is None:\n                if options:","sourceCodeStart":362,"sourceCodeEnd":398,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/widgets/_option_list.py#L362-L398","documentation":"A DuplicateID exception raised by OptionList.add_options (also used by set_options/add_option) when two or more Option entries in the batch share the same non-None id. Option ids must be unique within an OptionList so selections can be mapped back to options unambiguously.","triggerScenarios":"Passing [Option('a', id='x'), Option('b', id='x')] to add_options; building options from data with duplicate primary keys; calling add_options twice with the same id-bearing options.","commonSituations":"Generating menu options from database rows or API results containing duplicate ids; concatenating option lists that overlap; refresh routines appending already-present options.","solutions":["Deduplicate by id before adding: filter to first occurrence per id.","For refreshes, use set_options (replacing all) instead of add_options, or track existing ids.","Fix the upstream data to guarantee unique ids.","Catch DuplicateID and report which ids collided."],"exampleFix":"# before\noption_list.add_options(options)  # duplicate ids present\n\n# after\nseen = set()\nunique = [o for o in options if not (o.id and o.id in seen or seen.add(o.id))]\noption_list.add_options(unique)","handlingStrategy":"validation","validationCode":"seen: set[str] = set()\nunique = [o for o in options if o.id is None or not (o.id in seen or seen.add(o.id))]\noption_list.add_options(unique)","typeGuard":null,"tryCatchPattern":"from textual.widgets._option_list import DuplicateID\ntry:\n    option_list.add_options(options)\nexcept DuplicateID:\n    # dedupe and retry\n    seen, unique = set(), []\n    for o in options:\n        if o.id is None or o.id not in seen:\n            if o.id is not None:\n                seen.add(o.id)\n            unique.append(o)\n    option_list.add_options(unique)","preventionTips":["Deduplicate ids before add_options/set_options","Use set_options for full refreshes instead of appending","Guarantee unique ids at the data source (primary keys)"],"tags":["textual","option-list","duplicate-id","widget"],"backgroundTag":"duplicate-key-insert","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}