Regex (Perl / PCRE), 21 bytes
^(<(((?1),)*(?1))?>)$
Uses the characters <>,. (Avoids [] as they would require being \-escaped.)
Try it online! - Perl
Try it on regex101 - PCRE1
Try it online! - PCRE2 v10.33
Attempt This Online! - PCRE2 v10.39+
Like the regex in the Raku answer, this uses recursion. Unlike Raku, standard regex has no concept of "separator" characters, so there's no concise way of implementing a comma-separated list, and the recursive call (?1) needs to be in two places.
There are many alternatives of the same length:
Regex (Perl / PCRE), 21 bytes
^(<((?1)(,(?2))?)?>)$
Also uses the characters <>,.
Try it online! - Perl
Try it online! - PCRE
Uses (?2) recursion instead of * repetition for the comma-separated list.
Regex (Perl / PCRE), 21 bytes
^(a((?1),)*(?1)?\Bz)$
Uses the characters az, in order to take advantage of the \B non-word-boundary assertion.
Try it online! - Perl
Try it online! - PCRE
Without the use of \B, there would be nothing stopping a comma-separated list ending in a comma from being accepted, as the (?1)? is optional independently of whether or not ((?1),)* matched anything.
The \B prevents this; if any list ended with a comma, the sequence ,z would be part of it, so all we need to do is prohibit this sequence. \Bz accomplishes this, as a and z are word-characters but , is not, thus there is no word boundary in the middle of az or zz but there is one in ,z.
Regex (Perl / PCRE), 21 bytes
^(a\B(?1)?(,(?1))*z)$
Also uses the characters az,. Mirror version of the above.
Try it online! - Perl
Try it online! - PCRE
Regex (Perl / PCRE), 21 bytes
^(<(\B(?1)|\b,\B)*z)$
Try it online! - Perl
Try it online! - PCRE
Uses the characters <z, in order to take advantage of the \b and \B word- and non-word-boundary assertions.
Regex (Perl / PCRE), 21 bytes
^(a((?1)\B|\B,\b)*>)$
Uses the characters a>,. Mirror version of the above.
Try it online! - Perl
Try it online! - PCRE
Regex (.NET), 35 33 29 bytes
^((a)+(?<-2>z)+(?(2),\b|$))+$
Uses the characters az, in order to take advantage of the \b word-boundary assertion.
Try it online!
Based on the old 35 byte one-liner in Neil's Retina answer.
-1 byte by using the characters <>, instead of [],, because [ needed to be \-escaped
-1 byte by using an illegal character, instead of $., as the impossible condition to assert Group 2 being empty at the end
-4 bytes by using the characters az, instead of <>,, obviating the need for explicitly asserting Group 2 is empty at the end
^ # Assert that we're at the beginning of the string.
(
(a)+ # Capture at least one "a" on the Group 2 stack.
(?<-2>z)+ # Match at least one "z", popping an entry from the Group 2
# stack for each one we match.
(?(2),\b|$) # If the Group 2 stack is non-empty, match a "," followed by a
# word boundary (since "," is a non-word character, this means
# it must be followed by a word character, i.e. [0-9A-Za-z_]),
# else assert that we're at the end of the string.
)+ # Loop the above at least 1 time.
$ # Assert that we're at the end of the string. The Group 2
# conditional at the end of the above loop guarantees that the
# only way to end the loop at the end of the string is for
# Group 2 to be empty, due to the "\b" in its non-empty
# clause. So there's no need to explicitly assert here
# something like "(?(2)$.)" (which would assert something
# impossible in the case that Group 2 is non-empty).
Note that if the <>, characters are still used, it can be 32 bytes:
^((<)+(?<-2>>)+(?(2),(?!$)|$))+$
Try it online!
\$\large\textit{Anonymous functions}\$
Perl, 33 bytes
sub{pop=~/^(<(((?1),)*(?1))?>)$/}
Try it online!
R, 49 48 44 39 bytes
\(L)grepl('^(<(((?1),)*(?1))?>)$',L,,1)
Attempt This Online!
-1 byte thanks to Giuseppe
-4 bytes by using grepl() instead of sum(grep()) or any(grep())
-5 bytes by using a new anonymous function syntax introduced in R v4.1.0
$args-match'^((a)+(?<-2>z)+(?(2),\b|$))+$'
Try it online!
,[]\$\endgroup\$[[]\$\endgroup\$ListQ... which doesn't quite meet these specs for non-lists. \$\endgroup\$