{"id":"regex-repetition","slug":"repetition-and-groups","title":"Repetition and groups","text":"`\\d\\d\\d\\d` matches four digits, but writing the same part again and again gets tiring, and often you don't even know how many there will be. A **quantifier** right after a part of a pattern says how many times that part repeats:\n\n- `*` zero or more times\n- `+` one or more times\n- `?` zero times or once: the part is optional\n- `{m}` exactly m times\n- `{m,n}` from m to n times\n\nA quantifier repeats only the one thing right before it: a character, a class or a short form such as `\\d`. So `r\"ha+\"` is an h followed by one or more a's, and `r\"[0-9]+\"` is one or more digits:\n\n```pycon\n>>> import re\n>>> re.findall(r\"ha+\", \"ha haaa h hah\")\n['ha', 'haaa', 'ha']\n>>> re.findall(r\"colou?r\", \"color or colour?\")\n['color', 'colour']\n>>> re.findall(r\"\\d{4}\", \"1984, 42, 2026\")\n['1984', '2026']\n>>> re.findall(r\"[A-Z][a-z]*\", \"Ada met Bo and X\")\n['Ada', 'Bo', 'X']\n```\n\nThe last pattern is a capital letter followed by any number of small letters, none at all included, so it finds capitalized words.\n\n### Groups\n\nRound brackets make a **group**. They collect part of a pattern into one unit: a quantifier after a group repeats the whole group, and a `|` inside a group chooses only within it:\n\n```python\nimport re\n\nfor laugh in [\"haha\", \"hahaha\", \"hah\"]:\n    print(laugh, re.search(r\"^(ha)+$\", laugh) is not None)\nfor word in [\"gray\", \"grey\", \"groy\"]:\n    print(word, re.search(r\"^gr(a|e)y$\", word) is not None)\n```\n\n```output\nhaha True\nhahaha True\nhah False\ngray True\ngrey True\ngroy False\n```\n\nWithout the brackets, `r\"^ha+$\"` would be an h followed by a's, which matches `haaa` but not `haha`. And `r\"^gra|ey$\"` would mean \"starts with gra, or ends with ey\". One thing to know: when a pattern has a group, `re.findall` returns only the part that the group matched, not the whole match. So use groups with `re.search`, or with the function below.\n\n### The whole text: re.fullmatch\n\n`^` and `$` around a pattern make it describe the whole text. `re.fullmatch(pattern, text)` does the same more simply: it matches only when the pattern covers the text from its first character to its last. That makes it the natural tool for checking a whole value, such as a year:\n\n```python\nimport re\n\n\ndef is_year(text):\n    return re.fullmatch(r\"\\d{4}\", text) is not None\n\n\nfor text in [\"2026\", \"20266\", \"year 2026\", \"99\"]:\n    print(text, is_year(text))\n```\n\n```output\n2026 True\n20266 False\nyear 2026 False\n99 False\n```\n\n`re.search(r\"\\d{4}\", \"20266\")` would find a match, the first four digits. `re.fullmatch` doesn't, because a fifth digit is left over.\n","task":"A US ZIP code is exactly five digits. Fill in the quantifier so that the program prints:\n\n```\n90210 valid\n1234 invalid\n123456 invalid\n02139 valid\n```\n","kind":"fill","seed":"import re\n\nfor code in [\"90210\", \"1234\", \"123456\", \"02139\"]:\n    if re.fullmatch(r\"\\d[[{5}]]\", code):\n        print(code, \"valid\")\n    else:\n        print(code, \"invalid\")\n","solution":"import re\n\nfor code in [\"90210\", \"1234\", \"123456\", \"02139\"]:\n    if re.fullmatch(r\"\\d{5}\", code):\n        print(code, \"valid\")\n    else:\n        print(code, \"invalid\")\n","hints":["`\\d` is one digit. The quantifier after it says how many times it repeats.","Exactly five times is written `{5}`."],"tests":[{"name":"Output","stdin":"","expect":"90210 valid\n1234 invalid\n123456 invalid\n02139 valid","hidden":false}],"require":[],"forbid":[],"more":[{"task":"What does this program print? Type each line it prints.\n","kind":"predict","seed":"import re\n\nprint(re.findall(r\"o+\", \"foo bo boooo\"))\nprint(re.findall(r\"\\d{2,3}\", \"1 22 333 4444\"))\nprint(re.fullmatch(r\"(ab)+\", \"ababab\") is not None)\nprint(re.fullmatch(r\"ab+\", \"ababab\") is not None)\n","solution":"import re\n\nprint(re.findall(r\"o+\", \"foo bo boooo\"))\nprint(re.findall(r\"\\d{2,3}\", \"1 22 333 4444\"))\nprint(re.fullmatch(r\"(ab)+\", \"ababab\") is not None)\nprint(re.fullmatch(r\"ab+\", \"ababab\") is not None)\n","hints":["`o+` takes each run of o's as one match, as long as it can be.","`\\d{2,3}` takes two or three digits, as many as it can. In `4444` that leaves one digit, which is too few for another match.","`(ab)+` repeats the whole pair, but `ab+` repeats only the b."],"tests":[{"name":"Output","stdin":"","expect":"['oo', 'o', 'oooo']\n['22', '333', '444']\nTrue\nFalse","hidden":false}],"require":[],"forbid":[],"lines":["['oo', 'o', 'oooo']","['22', '333', '444']","True","False"]},{"task":"A product code is two capital letters, a dash and four digits, such as `AB-1234`. It may also end with a dash and one more capital letter, as in `AB-1234-X`. Write a function `is_product_code(text)` that returns `True` if the whole text is a product code, and `False` otherwise. Use `re.fullmatch`. For example,\n\n```python\nprint(is_product_code(\"AB-1234\"))\nprint(is_product_code(\"AB-1234-X\"))\nprint(is_product_code(\"AB-123\"))\n```\n\nshould print:\n\n```\nTrue\nTrue\nFalse\n```\n","kind":"code","seed":"import re\n\n\ndef is_product_code(text):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(is_product_code(\"AB-1234\"))\n    print(is_product_code(\"AB-1234-X\"))\n    print(is_product_code(\"AB-123\"))\n","solution":"import re\n\n\ndef is_product_code(text):\n    return re.fullmatch(r\"[A-Z]{2}-\\d{4}(-[A-Z])?\", text) is not None\n\n\nif __name__ == \"__main__\":\n    print(is_product_code(\"AB-1234\"))\n    print(is_product_code(\"AB-1234-X\"))\n    print(is_product_code(\"AB-123\"))\n","hints":["Two capital letters are `[A-Z]{2}`, and four digits are `\\d{4}`. A dash outside square brackets is an ordinary character.","The ending is optional: put the dash and the letter in a group and add `?` after it: `(-[A-Z])?`.","`re.fullmatch` returns a match or `None`; return `... is not None`."],"tests":[{"name":"Test 1","stdin":"","call":"print(is_product_code(\"AB-1234\"))\nprint(is_product_code(\"AB-1234-X\"))\nprint(is_product_code(\"AB-123\"))\n","expect":"True\nTrue\nFalse","hidden":false},{"name":"Test 2","stdin":"","call":"print(is_product_code(\"ab-1234\"))\nprint(is_product_code(\"AB1234\"))\nprint(is_product_code(\"ZZ-0000\"))\nprint(is_product_code(\"AB-1234-\"))\nprint(is_product_code(\"AB-1234-XY\"))\nprint(is_product_code(\"QR-9876-K\"))\nprint(is_product_code(\"ABC-1234\"))\n","expect":"False\nFalse\nTrue\nFalse\nFalse\nTrue\nFalse","hidden":true}],"require":[{"pattern":"\\bre\\.fullmatch\\s*\\(","message":"Check the whole code with `re.fullmatch`."}],"forbid":[]}]}