diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 531ca31..cf1534c 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 1.0.2
+
+- **FIX**: Use proper CSS identifier patterns for tag names, classes, ids, etc. Things like `#3` or `#-3` should not match and should require `#\33` or `#-\33`.
+- **FIX**: Do not raise `NotImplementedError` for supported pseudo classes/elements with bad syntax, instead raise `SyntaxError`.
+
 ## 1.0.1
 
 - **FIX**: When giving a tag to `select`, it should only return the children of that tag, never the tag itself.
diff --git a/soupsieve/__meta__.py b/soupsieve/__meta__.py
index 4c3c35d..77a37c1 100644
--- a/soupsieve/__meta__.py
+++ b/soupsieve/__meta__.py
@@ -186,5 +186,5 @@ def parse_version(ver, pre=False):
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(1, 0, 1, "final")
+__version_info__ = Version(1, 0, 2, "final")
 __version__ = __version_info__._get_canonical()
diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py
index b605383..d325f2a 100644
--- a/soupsieve/css_parser.py
+++ b/soupsieve/css_parser.py
@@ -6,29 +6,58 @@ from . import css_match as cm
 from . import css_types as ct
 from collections import OrderedDict
 
+SUPPORTED_PSEUDO = {
+    ":root",
+    ":empty",
+    ":has",
+    ":is",
+    ":matches",
+    ":not",
+    ":where",
+    ":first-child",
+    ":first-of-type",
+    ":last-child",
+    ":last-of-type",
+    ":only-child",
+    ":only-of-type",
+    ":nth-child",
+    ":nth-last-child",
+    ":nth-of-type",
+    ":nth-last-of-type"
+}
 
 # Sub-patterns parts
 WS = r'[ \t\r\n\f]'
 
 CSS_ESCAPES = r'(?:\\[a-f0-9]{{1,6}}{ws}?|\\.)'.format(ws=WS)
 
+IDENTIFIER = r'''
+(?:(?!-?\d|--)(?:[^\u0000-\u002c\u002e\u002f\u003A-\u0040\u005B-\u005E\u0060\u007B-\u009f]|{esc})+)
+'''.format(esc=CSS_ESCAPES)
+
 NTH = r'(?:[-+])?(?:\d+n?|n)(?:(?<=n){ws}*(?:[-+]){ws}*(?:\d+))?'.format(ws=WS)
 
-VALUE = r'''(?P<value>"(?:\\.|[^\\"]+)*?"|'(?:\\.|[^\\']+)*?'|(?:[^'"\[\] \f\t\r\n]|{esc})+)'''.format(esc=CSS_ESCAPES)
+VALUE = r'''
+(?P<value>
+    "(?:\\.|[^\\"]+)*?"|
+    '(?:\\.|[^\\']+)*?'|
+    {ident}+
+)
+'''.format(ident=IDENTIFIER)
 
 ATTR = r'''(?:{ws}*(?P<cmp>[~^|*$]?=){ws}*{value}(?P<case>{ws}+[is])?)?{ws}*\]'''.format(ws=WS, value=VALUE)
 
 # Selector patterns
-PAT_ID = r'#(?:[-\w]|{esc})+'.format(esc=CSS_ESCAPES)
+PAT_ID = r'\#{ident}'.format(ident=IDENTIFIER)
 
-PAT_CLASS = r'\.(?:[-\w]|{esc})+'.format(esc=CSS_ESCAPES)
+PAT_CLASS = r'\.{ident}'.format(ident=IDENTIFIER)
 
-PAT_TAG = r'(?:(?:(?:[-\w]|{esc})+|\*)?\|)?(?:(?:[-\w]|{esc})+|\*)'.format(esc=CSS_ESCAPES)
+PAT_TAG = r'(?:(?:{ident}|\*)?\|)?(?:{ident}|\*)'.format(ident=IDENTIFIER)
 
-PAT_ATTR = r'''(?x)
-\[{ws}*(?P<ns_attr>(?:(?:(?:[-\w]|{esc})+|\*)?\|)?(?:[-\w]|{esc})+)
+PAT_ATTR = r'''
+\[{ws}*(?P<ns_attr>(?:(?:{ident}|\*)?\|)?{ident})
 {attr}
-'''.format(ws=WS, esc=CSS_ESCAPES, attr=ATTR)
+'''.format(ws=WS, ident=IDENTIFIER, attr=ATTR)
 
 PAT_PSEUDO_OPEN = r':(?:has|is|matches|not|where)\('
 
@@ -36,22 +65,22 @@ PAT_PSEUDO_CLOSE = r'{ws}*\)'.format(ws=WS)
 
 PAT_PSEUDO = r':(?:empty|root|(?:first|last|only)-(?:child|of-type))\b'
 
-PAT_PSEUDO_NTH_CHILD = r'''(?x)
+PAT_PSEUDO_NTH_CHILD = r'''
 (?P<pseudo_nth_child>:nth-(?:last-)?child
 \({ws}*(?P<nth_child>{nth}|even|odd){ws}*(?:\)|(?<={ws})of{ws}+))
 '''.format(ws=WS, nth=NTH)
 
-PAT_PSEUDO_NTH_TYPE = r'''(?x)
+PAT_PSEUDO_NTH_TYPE = r'''
 (?P<pseudo_nth_type>:nth-(?:last-)?of-type
 \({ws}*(?P<nth_type>{nth}|even|odd){ws}*\))
 '''.format(ws=WS, nth=NTH)
 
 PAT_SPLIT = r'{ws}*?(?P<relation>[,+>~]|{ws}(?![,+>~])){ws}*'.format(ws=WS)
 
-PAT_INVALID_PSEUDO = r':[a-z0-9-]+'
-
 # Extra selector patterns
-PAT_CONTAINS = r':contains\({ws}*{value}{ws}*\)'.format(ws=WS, value=VALUE)
+PAT_PSEUDO_CONTAINS = r':contains\({ws}*{value}{ws}*\)'.format(ws=WS, value=VALUE)
+
+PAT_PSEUDO_INVALID = r':{ident}'.format(ident=IDENTIFIER)
 
 # CSS escape pattern
 RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\.))'.format(ws=WS), re.I)
@@ -100,7 +129,7 @@ class SelectorPattern:
     def __init__(self, pattern):
         """Initialize."""
 
-        self.pattern = re.compile(pattern, re.I)
+        self.pattern = re.compile(pattern, re.I | re.X)
 
     def enabled(self, flags):
         """Enabled."""
@@ -179,16 +208,16 @@ class CSSParser:
     css_tokens = OrderedDict(
         [
             ("pseudo_open", SelectorPattern(PAT_PSEUDO_OPEN)),
+            ("pseudo_close", SelectorPattern(PAT_PSEUDO_CLOSE)),
             ("pseudo", SelectorPattern(PAT_PSEUDO)),
-            ("contains", SelectorPattern(PAT_CONTAINS)),
+            ("pseudo_contains", SelectorPattern(PAT_PSEUDO_CONTAINS)),
             ("pseudo_nth_child", SelectorPattern(PAT_PSEUDO_NTH_CHILD)),
             ("pseudo_nth_type", SelectorPattern(PAT_PSEUDO_NTH_TYPE)),
-            ("pseudo_invalid", SelectorPattern(PAT_INVALID_PSEUDO)),
+            ("pseudo_invalid", SelectorPattern(PAT_PSEUDO_INVALID)),
             ("id", SelectorPattern(PAT_ID)),
             ("class", SelectorPattern(PAT_CLASS)),
             ("tag", SelectorPattern(PAT_TAG)),
             ("attribute", SelectorPattern(PAT_ATTR)),
-            ("pseudo_close", SelectorPattern(PAT_PSEUDO_CLOSE)),
             ("combine", SelectorPattern(PAT_SPLIT))
         ]
     )
@@ -467,10 +496,15 @@ class CSSParser:
 
                 # Handle parts
                 if key == 'pseudo_invalid':
-                    raise NotImplementedError("'{}' pseudo class is not implemented".format(m.group(0)))
+                    if util.lower(m.group(0)) in SUPPORTED_PSEUDO:
+                        raise SyntaxError("Invalid syntax for pseudo class '{}'".format(key))
+                    else:
+                        raise NotImplementedError(
+                            "'{}' pseudo class/element is not implemented at this time".format(key)
+                        )
                 elif key == 'pseudo':
                     has_selector = self.parse_pseudo(sel, m, has_selector)
-                elif key == 'contains':
+                elif key == 'pseudo_contains':
                     has_selector = self.parse_contains(sel, m, has_selector)
                 elif key in ('pseudo_nth_type', 'pseudo_nth_child'):
                     has_selector = self.parse_pseudo_nth(sel, m, has_selector, iselector)

Use proper CSS identifier patterns for class, id, tags, etc.
Right now we allow things like `#3` which isn't really allowed in CSS.  We should use proper patterns that mimic CSS appropriately.
diff --git a/tests/test_level1.py b/tests/test_level1.py
index 7eb24dd..af8e923 100644
--- a/tests/test_level1.py
+++ b/tests/test_level1.py
@@ -82,14 +82,14 @@ class TestLevel1(util.TestCase):
 
         self.assert_selector(
             markup,
-            "#1",
+            "#\\31",
             ["1"],
             flags=util.HTML5
         )
 
         self.assert_selector(
             markup,
-            "a#2",
+            "a#\\32",
             ["2"],
             flags=util.HTML5
         )
diff --git a/tests/test_level2.py b/tests/test_level2.py
index eedd928..3d615b4 100644
--- a/tests/test_level2.py
+++ b/tests/test_level2.py
@@ -95,7 +95,7 @@ class TestLevel2(util.TestCase):
         # Complex
         self.assert_selector(
             markup,
-            "span#4 + span#5",
+            "span#\\34 + span#\\35",
             ["5"],
             flags=util.HTML5
         )
@@ -193,7 +193,7 @@ class TestLevel2(util.TestCase):
         # No quotes
         self.assert_selector(
             markup,
-            '[id=5]',
+            '[id=\\35]',
             ["5"],
             flags=util.HTML5
         )
diff --git a/tests/test_level4.py b/tests/test_level4.py
index acc43d6..0c2a92f 100644
--- a/tests/test_level4.py
+++ b/tests/test_level4.py
@@ -162,14 +162,14 @@ class TestLevel4(util.TestCase):
 
         self.assert_selector(
             markup,
-            ":is(span, a:matches(#2))",
+            ":is(span, a:matches(#\\32))",
             ["1", "2"],
             flags=util.HTML5
         )
 
         self.assert_selector(
             markup,
-            ":where(span, a:matches(#2))",
+            ":where(span, a:matches(#\\32))",
             ["1", "2"],
             flags=util.HTML5
         )
@@ -196,7 +196,7 @@ class TestLevel4(util.TestCase):
         # So this will match
         self.assert_selector(
             markup,
-            ":is(a):is(#2)",
+            ":is(a):is(#\\32)",
             ['2'],
             flags=util.HTML5
         )
@@ -219,7 +219,7 @@ class TestLevel4(util.TestCase):
 
         self.assert_selector(
             markup,
-            'div :not(p, :not([id=5]))',
+            'div :not(p, :not([id=\\35]))',
             ['5'],
             flags=util.HTML5
         )
diff --git a/tests/test_soupsieve.py b/tests/test_soupsieve.py
index 26d9a3b..6d43290 100644
--- a/tests/test_soupsieve.py
+++ b/tests/test_soupsieve.py
@@ -101,8 +101,8 @@ class TestSoupSieve(unittest.TestCase):
 
         soup = bs4.BeautifulSoup(markup, 'html5lib')
         nodes = sv.select('span[id]', soup)
-        self.assertTrue(sv.match('span#5', nodes[0]))
-        self.assertFalse(sv.match('span#5', nodes[1]))
+        self.assertTrue(sv.match('span#\\35', nodes[0]))
+        self.assertFalse(sv.match('span#\\35', nodes[1]))
 
     def test_filter(self):
         """Test filter."""
@@ -125,11 +125,11 @@ class TestSoupSieve(unittest.TestCase):
         """
 
         soup = bs4.BeautifulSoup(markup, 'html5lib')
-        nodes = sv.filter('pre#6', soup.html.body)
+        nodes = sv.filter('pre#\\36', soup.html.body)
         self.assertEqual(len(nodes), 1)
         self.assertEqual(nodes[0].attrs['id'], '6')
 
-        nodes = sv.filter('pre#6', [el for el in soup.html.body.children if isinstance(el, bs4.Tag)])
+        nodes = sv.filter('pre#\\36', [el for el in soup.html.body.children if isinstance(el, bs4.Tag)])
         self.assertEqual(len(nodes), 1)
         self.assertEqual(nodes[0].attrs['id'], '6')
 
@@ -167,7 +167,7 @@ class TestSoupSieve(unittest.TestCase):
         sv.purge()
         self.assertEqual(sv.cp._cached_css_compile.cache_info().currsize, 0)
         for x in range(1000):
-            value = '[value={}]'.format(str(random.randint(1, 10000)))
+            value = '[value="{}"]'.format(str(random.randint(1, 10000)))
             p = sv.compile(value)
             self.assertTrue(p.pattern == value)
             self.assertTrue(sv.cp._cached_css_compile.cache_info().currsize > 0)
@@ -370,6 +370,9 @@ class TestInvalid(unittest.TestCase):
         with self.assertRaises(NotImplementedError):
             sv.compile(':before')
 
+        with self.assertRaises(SyntaxError):
+            sv.compile(':nth-child(a)')
+
     def test_invalid_pseudo_close(self):
         """Test invalid pseudo close."""
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest coverage flake8 flake8-docstrings flake8-builtins flake8-mutable pymdown-extensions mkdocs mkdocs-material
apt-get update apt-get install -y gcc aspell aspell-en
3.6
requirements/project.txt requirements/tests.txt requirements/flake8.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
beautifulsoup4==4.6.3
certifi==2021.5.30
click==8.0.4
coverage==6.2
flake8==5.0.4
flake8-builtins==2.0.0
flake8-docstrings==1.6.0
flake8-mutable==1.2.0
ghp-import==2.1.0
html5lib==1.1
importlib-metadata==4.2.0
iniconfig==1.1.1
Jinja2==3.0.3
lxml==5.3.1
Markdown==3.3.4
MarkupSafe==2.0.1
mccabe==0.7.0
mergedeep==1.3.4
mkdocs==1.2.4
mkdocs-material==8.2.7
mkdocs-material-extensions==1.0.3
packaging==21.3
pep8-naming==0.13.1
pluggy==1.0.0
py==1.11.0
pycodestyle==2.9.1
pydocstyle==6.3.0
pyflakes==2.5.0
Pygments==2.14.0
pymdown-extensions==9.1
pyparsing==3.1.4
pytest==7.0.1
pytest-cov==4.0.0
python-dateutil==2.9.0.post0
PyYAML==6.0.1
pyyaml_env_tag==0.1
six==1.17.0
snowballstemmer==2.2.0
-e git+https://github.com/facelessuser/soupsieve.git@1756b285b9ae13a702830a7e8cc9b7dff8361b40#egg=soupsieve
tomli==1.2.3
typing_extensions==4.1.1
watchdog==2.3.1
webencodings==0.5.1
zipp==3.6.0

name: soupsieve
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - beautifulsoup4==4.6.3
      - click==8.0.4
      - coverage==6.2
      - flake8==5.0.4
      - flake8-builtins==2.0.0
      - flake8-docstrings==1.6.0
      - flake8-mutable==1.2.0
      - ghp-import==2.1.0
      - html5lib==1.1
      - importlib-metadata==4.2.0
      - iniconfig==1.1.1
      - jinja2==3.0.3
      - lxml==5.3.1
      - markdown==3.3.4
      - markupsafe==2.0.1
      - mccabe==0.7.0
      - mergedeep==1.3.4
      - mkdocs==1.2.4
      - mkdocs-material==8.2.7
      - mkdocs-material-extensions==1.0.3
      - packaging==21.3
      - pep8-naming==0.13.1
      - pluggy==1.0.0
      - py==1.11.0
      - pycodestyle==2.9.1
      - pydocstyle==6.3.0
      - pyflakes==2.5.0
      - pygments==2.14.0
      - pymdown-extensions==9.1
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.1
      - pyyaml-env-tag==0.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - watchdog==2.3.1
      - webencodings==0.5.1
      - zipp==3.6.0
prefix: /opt/conda/envs/soupsieve

tests/test_soupsieve.py::TestInvalid::test_invalid_pseudo
tests/test_level1.py::TestLevel1::test_child tests/test_level1.py::TestLevel1::test_class tests/test_level1.py::TestLevel1::test_classes tests/test_level1.py::TestLevel1::test_escapes tests/test_level1.py::TestLevel1::test_id tests/test_level1.py::TestLevel1::test_tag tests/test_level1.py::TestLevel1::test_tags tests/test_level2.py::TestLevel2::test_attribute tests/test_level2.py::TestLevel2::test_attribute_contains_space tests/test_level2.py::TestLevel2::test_attribute_equal tests/test_level2.py::TestLevel2::test_attribute_start_dash tests/test_level2.py::TestLevel2::test_attribute_type tests/test_level2.py::TestLevel2::test_direct_child tests/test_level2.py::TestLevel2::test_direct_sibling tests/test_level2.py::TestLevel2::test_first_child tests/test_level2.py::TestLevel2::test_multi_attribute tests/test_level2.py::TestLevel2::test_wild_tag tests/test_level4.py::TestLevel4::test_attribute_case tests/test_level4.py::TestLevel4::test_attribute_type_case tests/test_level4.py::TestLevel4::test_has tests/test_level4.py::TestLevel4::test_is_matches_where tests/test_level4.py::TestLevel4::test_multi_nested_not tests/test_level4.py::TestLevel4::test_nth_child_of_s tests/test_soupsieve.py::TestSoupSieve::test_cache tests/test_soupsieve.py::TestSoupSieve::test_comments tests/test_soupsieve.py::TestSoupSieve::test_copy_pickle tests/test_soupsieve.py::TestSoupSieve::test_filter tests/test_soupsieve.py::TestSoupSieve::test_immutable_dict tests/test_soupsieve.py::TestSoupSieve::test_immutable_object tests/test_soupsieve.py::TestSoupSieve::test_match tests/test_soupsieve.py::TestSoupSieve::test_recompile tests/test_soupsieve.py::TestSoupSieve::test_select tests/test_soupsieve.py::TestDeprcations::test_commentsiter_deprecation tests/test_soupsieve.py::TestDeprcations::test_flag_deprecations tests/test_soupsieve.py::TestDeprcations::test_selectiter_deprecation tests/test_soupsieve.py::TestInvalid::test_invalid_combination tests/test_soupsieve.py::TestInvalid::test_invalid_incomplete_has tests/test_soupsieve.py::TestInvalid::test_invalid_namespace tests/test_soupsieve.py::TestInvalid::test_invalid_pseudo_close tests/test_soupsieve.py::TestInvalid::test_invalid_pseudo_open tests/test_soupsieve.py::TestInvalid::test_invalid_syntax tests/test_soupsieve.py::TestInvalid::test_invalid_tag
soupsieve/__meta__.py docs/src/markdown/about/changelog.md soupsieve/css_parser.py
soupsieve/__meta__.py docs/src/markdown/about/changelog.md soupsieve/css_parser.py