Regex to match several slightly different strings
This isn't so much a Python question as it is a RegEx question.  The Python tag is almost irrelevant.
'+' -vs- '*'
You regex includes two alternates:

(?:20\d\d#\d{1,3}(?:,\d{1,3})+)
(?:20\d\d#\d{1,3})

The difference in these expressions is the first one include one or more repeats of ,\d{1,3}, where as the second does not include any repeats of that ...
... in other words, you have 20\d\d#\d{1,3} followed by 0 or more repeats of ,\d{1,3}.
So we can replace the + modifier with the * modifier, and the pair of alternates becomes simply
(?:20\d\d#\d{1,3}(?:,\d{1,3})*)
Common prefix
All three (or rather, both now that the last two have been combined into one) regex's start with 20\d\d#.  You can factor this out of both patterns.
20\d\d# (?: (?: \d{1,3}-\d{1,3} | \d{1,3}(?:,\d{1,3})* ) )
Capture Groups
There is no point creating a capture group for the entire regex expression.  The entire match is always captured, as group 0.
Reworked Regex &amp; Example
The regex can be rewritten as:
r&quot;20\d\d#(?:(?:\d{1,3}-\d{1,3}|\d{1,3}(?:,\d{1,3})*))&quot;
which is shorter, but is still quite unreadable.  It is better for future programmers at the charity (as well as for yourself) to document the regex using the re.VERBOSE option.  I find it clearer than comments below the regular expression that try to document the parts of the regular expression through vertically lined up notes.
Note: Since comments follow the octothorpe (#) character in re.VERBOSE mode, we need to escape it (\#) to indicate that it is actually a character we want matched, and not the start of a comment.
import re

JOB_PATTERN = re.compile(r&quot;&quot;&quot;
                          20\d\d                     # A 4-digit year
                          \#                         # followed by a hash mark
                          (?:                        # followed by either ...
                             (?:\d{1,3}-\d{1,3})        # a range of serial #'s
                             |                          # or
                             (?:\d{1,3}(?:,\d{1,3})*)   # a list of serial #'s
                          )&quot;&quot;&quot;, re.VERBOSE)


test_cases = [&quot;junk2021#123-45bar&quot;,
              &quot;baz2021#003,5,07,89,131foo&quot;]

for test_case in test_cases:
    m = JOB_PATTERN.search(test_case)
    if m:
        print(&quot;Found:&quot;, m[0])

Output:
Found: 2021#123-45
Found: 2021#003,5,07,89,131

Update
Based on the OP's response to RootTwo's comment, named capturing groups are actually desired.  Here is an updated example with RootTwo's enhancement:
import re

JOB_PATTERN = re.compile(
    r&quot;&quot;&quot;(?P&lt;year&gt;              # Capturedas &quot;year&quot; ...
           20\d\d                 # a 4-digit year
        )
        \#                     # followed by a hash mark
        (?:                    # followed by either ...
           (?P&lt;range&gt;             # captured as &quot;range&quot; ...
              \d{1,3}-\d{1,3}        # a range of serial #'s
           )
           |                      # or
           (?P&lt;list&gt;              # captured as &quot;list&quot; ...
               \d{1,3}               # a serial number
               (?:,\d{1,3})*         # or list of comma-seperated serial #'s
           )
        )
        &quot;&quot;&quot;, re.VERBOSE)

test_cases = [&quot;junk2021#123-45bar&quot;,
              &quot;baz2021#003,5,07,89,131foo&quot;]

for test_case in test_cases:
    m = JOB_PATTERN.search(test_case)
    if m:
        if m.lastgroup == &quot;range&quot;:
            start, end = m['range'].split('-')
            print(&quot;Year:&quot;, m['year'], &quot; from&quot;, start, &quot;to&quot;, end)
        else:
            serial_numbers = m['list'].split(',')
            print(&quot;Year:&quot;, m['year'], &quot; serial #':&quot;, serial_numbers)

Output:
Year: 2021  from 123 to 45
Year: 2021  serial #': ['003', '5', '07', '89', '131']