Repository: facelessuser/wcmatch (language: python)

Issue:
wcmatch is missing typehints
While trying to adopt the wmatch I discovered that it does not have type hints. Adding them would really be appreciated as we do want to write better Python code.

```
# As reported by mypy:
 error: Skipping analyzing 'wcmatch.pathlib': found module but no type hints or library stubs
```


Discussion / hints:
I come from a C/C++ background, so I do understand the arguments for type hints and such. While I've considered looking into them, they also haven't been a high priority. I don't feel that not having them detracts from the usefulness of wcmatch, but I can understand that for those that use IDEs and such, type hinting can be desirable.

@gir-bot remove S: triage
@gir-bot add T: enhancement
I am also okay with people who'd like to assist in this endeavor.
@facelessuser Thank for the update. Adding type hints for libraries requiring py3.6+ is not very hard, maybe a slow process but not hard. I will try to assist on this, or at least to give a good start.
@ssbarnea  Most of it should be straight forward, but there are a couple of tricky types.

One type I am concerned with is the fact that the `wcmatch.glob.globfilter` library can accept lists of strings (byte or Unicode) and pathlike objects (specifically our derived Pathlib objects and Python's built-in Pathlib objects). WcMatch derives our custom Pathlibs from `pathlib.Path` and `pathlib.PurePath`, but I'm not sure how Python's typing works with these derived properties. Can I just type `pathlib.Path` and `pathlib.PurePath` and the classes derived from it will work?
I imagine at first blush, something like this may work, but I haven't experimented much with this.

```py
"""Types."""
from typing import Union, Any
import pathlib

# Wild card patterns can be strings or lists of strings
Wildcards = Union[str, bytes, list[str], list[bytes]]
# File names are strings or byte strings
Names = Union[str, bytes]
NamesList = list[Names]
# Glob paths can be strings, bytes, or pathlib objects
Paths = Union[str, bytes, pathlib.Path, pathlib.PurePath]
PathList = list[Paths]
```
It looks like it is not just as simple as adding types, but you have to refactor parts of the code that is handling some dynamic types so the typing checks understand what you are doing. Ugh. There is definitely some complexity to sort out to make this happen.


Another complexity is allowing iterables but not strings (which are iterable). I'll keep playing with this, but it is definitely more painful than I'd like.
Typings can't handle ternary operators 🤦.

```py
self.var: type = this if condition else that
```

This typing sure makes Python less fun to program in.
I'll rephrase, typings can't handle ternary operations that have any level of complexity. For instance, we used tuples in `this` and `that` (in the above example) and had a `type` that represented the tuple we wanted, mypy threw errors until we refactored into a non-ternary operation.

We also setup some variables as either bytes or strings, depending on the input. So we try to use types such as `var: Union[str, bytes]`. The problem is if you try to feed any of those into something like:

```
mystring.endswith(var)
```

It'll blow up in your face. Technically, it isn't wrong, in relation to types. `endswith` can't take  `bytes` or a `str`. We know it's okay, but mypy doesn't. This is were we start draining the joy out of using Python, and make it more C like. Not that C/C++ typing doesn't have its strengths. But you have to design differently if you are aiming for static typing vs dynamic.

So basically, our nice little string abstractions don't place nice with typing. Yes there are other ways to do it, but I'm going to have to refactor.

Are we unit tested quite thoroughly, yep. Are we coded to handle such cases properly? Yep. Will trying to type check this in mypy cause me to have a brain aneurysm, possibly.

You sacrifice some of the dynamic ease of Python to gain typing. Is your code better with typing? Well, how good are your tests? If your tests are good and thorough, you are probably okay. If they aren't, sure typing can help avoid dumb mistakes, but it won't prevent all cases.

I realize though, the ask isn't to ensure `wcmatch` is coded properly, but more to help others catch when they are passing bad things into the library.

I have no doubt I can solve all this, but it comes at a price: refactoring and time. I only spent a little time trying to type the `glob.py` file, and while I'm close, it is tedious and painful, and the last bit takes more work than the initial work. And then I still have to do the other submodules. I may have started with the hardest one though, as I think the handling in most of the others is easier to type.
Don't spend time on it. Let me do something when I have bit of time as I already built some experience in that. Adding type information can easily become a full-time job and finding solutions does require a lot of digging.

The reality is that since I started to use typing abour an year ago, I was forced to do a lot of refactoring in order to make mypy happy. This reduced the amount of magic and removed few bugs, still it was not really a pleasure to do it.

There are huge benefits for consumers of a library when you add typing, as this documents the code and makes it much easier to code (especially in IDEs). Extra bonus I no longer need to write doc strings,
most of the time.

Typing does not require dropping duck-typing, just adding more attention when doing it with few safe-guards, usually invoving converting alternative types to the preffered type so the following code uses a clearly determined type. 

Does it worth? Long term yes, short term likely not.
I have a start here: https://github.com/facelessuser/wcmatch/pull/151. I was able to get the glob library type hinted. Linting is broken, but unit tests are passing.

I support a number of projects, so learning this is something I'm interested in, so I pushed through it. I'm open though to review on it though. Insight from someone with experience is appreciated.
I guess this is expected, but it seems there are considerations for Python < 3.9 that I must make.
Yup, I am officially convinced that I don't know what I'm doing in regards to static typing in Python. For now, I'm leaving it as duck typing. I wasn't doing what I thought I was doing. It feels like it is intuitive until you start to do it, then it starts to feel much less so.

I imagine with projects that simple, straightforward types this is easy, but I'm finding this project to not be a good starting point to become familiar with the ins and outs of Python's static typing.

I'd like to not refactor all the guts just to get static typing. I may play around with this in the future if someone doesn't get to it first, but I should probably start with some of my less dynamic projects before I tackle this one.
At the very least, maybe I'll look at just getting the public API type hinted. I think that would give the most benefit to the community.
I took another stab at it, and this time it went much, much better. All the public interface (and then some) is hinted. I avoided the messy bits in `util.py` and avoided pretty much all of `_wcparse.py`.

It passes type hinting checks with mypy (avoiding disabling just what makes sense due to us not having the entirety hinted yet):

```
mypy --strict --no-warn-return-any --ignore-missing-imports --allow-untyped-calls \
    --allow-untyped-defs --no-check-untyped-defs --no-warn-unused-ignores wcmatch
```

There was some refactoring in `wcmatch.wcmatch` and even milder in other places. Now that I understand how the hinting works better (and ways it gets tripped up) it was much easier this time.

With that said, getting to 100% type hinting (the private stuff in the core) is going to be quite tricky as there is a bit of str/bytes abstraction and such. But that would be way down on the priority list. I think what we have now is a nice compromise.
Pull is at #153 
While I have at the top level things mostly typed, I'm going to give my experience with this. As someone who very much enjoys coding in Python, I very much don't love the typing system in Python. I'd rather write a library in C/C++ right now (a language designed with typing built-in).

- It seems typing is all handled with this MyPy linter, which is weird. Since it doesn't actually run the code, it can't tell what you are actually doing. So it gets confused ALL the time.

- You pull in a ton of noise as you pull in the world from `typing`.

- At the top level, things go fine. We need to accept any string type and sequences of string types, so we specify things like `Union[AnyStr, Sequence[AnyStr]]`. This is fine, and we may return `AnyStr`. But then there may be another layer, and you may also accept `Union[AnyStr, Sequence[AnyStr]]`, but only on variable needs that. Because `AnyStr` is generic, unless something else is using `AnyStr` in the function signature, it won't work right. You try to then use something compatible that handles bytes and strs, but then it isn't compatible with `Union[AnyStr, Sequence[AnyStr]]`, even though they are basically accepting the same thing.

- Typing can't handle ternary operations very well. Often we can't do things like:

    ```py
    var = b"bytes" if is_bytes else "str"
    ```

    One, mypy will forget what `is_bytes` is, two, it will call `var` an object type. What!?
    If you remove the ternary, and maybe use `isinstance(somevar, bytes)` in the conditional,
    mypy will have an easier time, but it still seems to get confused at times requiring
    even more annotations.

It's this comment that makes me question how worthwhile it is to add typing:

> Adding type information can easily become a full-time job and finding solutions does require a lot of digging.

Often, I feel like I'm coding to the mypy linter than ensuring good typing. If you design a project from the start with the `typing` module in mind, or you are dealing with a project with very simple typing to start, I can see it being non-trivial to add typing. I was even able to convert the [`bracex`](https://github.com/facelessuser/bracex) dependency which has only minimal byte/str abstraction (the changes are only local though as I have not committed and am still unsure if I will).

The amount of mental gymnastics here is frustrating. Could I eventually work through these issues? Sure, probably. Does it make this project easier to maintain? I would argue no. I don't want figuring out typing to be my full-time job, I want intuitive typing or sticking with duck typing. Whatever this typing feature is, it isn't real typing.

Considering the manifesto of python (https://www.python.org/dev/peps/pep-0020/), I'm curious why they went down a path so frustratingly obtuse. It's not that I have no familiarity with strongly typed languages, it's that typing isn't intuitive in Python, and since it is not built-in, the analyzing is fragile.

Well, I'll end my rant. I'm not sure if I want to ugly the code with half typed code that gets increasingly more difficult to handle as we try to increase typing. Maybe I'll consider stubs. I at least have a branch that people can kind of work off of, though it probably needs to be rebased, but maybe stubs are the better way to go, if we do this.
I'm flagging this with a "maybe" as the actual value for the project is low is here. People may want type hints in their editors, and I can understand that, but currently, for the project itself, it adds very little actual value.

We've documented the API quite well, so there shouldn't be any big surprises as far as inputs go.

So, this will be flagged as "maybe" with the likeliness increasing if there is assistance.

@gir-bot add P: maybe
I recently got `bracex` (which this project relies on) to work with type hints. I'm pretty confident now we could, at the very least, get types working for the API. I understand now how to package them properly and such. We could do this with `pyi` files if we don't want to type the entire lib. If we try and type the whole lib, I imagine we'll run into some of the same complexities we did before, but people really only care about having type hints for the API. I may be in our interest to start there and just do some `pyi` stub files.

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3b24399..bd65819 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,7 +17,7 @@ jobs:
       max-parallel: 4
       matrix:
         platform: [ubuntu-latest, windows-latest]
-        python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-beta.1]
+        python-version: [3.6, 3.7, 3.8, 3.9, '3.10']
         include:
           - python-version: 3.6
             tox-env: py36
@@ -27,11 +27,8 @@ jobs:
             tox-env: py38
           - python-version: 3.9
             tox-env: py39
-          - python-version: 3.10.0-beta.1
+          - python-version: '3.10'
             tox-env: py310
-        exclude:
-          - platform: windows-latest
-            python-version: 3.10.0-beta.1
 
     env:
       TOXENV: ${{ matrix.tox-env }}
diff --git a/.pyspelling.yml b/.pyspelling.yml
index d0f7f09..a12e3ad 100644
--- a/.pyspelling.yml
+++ b/.pyspelling.yml
@@ -67,7 +67,7 @@ matrix:
       context_visible_first: true
       delimiters:
       # Ignore lint (noqa) and coverage (pragma) as well as shebang (#!)
-      - open: '^(?: *(?:noqa\b|pragma: no cover)|!)'
+      - open: '^(?: *(?:noqa\b|pragma: no cover|type: .*?)|!)'
         close: '$'
       # Ignore Python encoding string -*- encoding stuff -*-
       - open: '^ *-\*-'
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index e27fb7f..c2b78d1 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## 8.3
+
+- **NEW**: Officially support Python 3.10.
+- **NEW**: Provide type hints for API.
+- **FIX**: Gracefully handle calls with an empty pattern list.
+
 ## 8.2
 
 - **NEW**: Add support for `dir_fd` in glob patterns.
diff --git a/setup.py b/setup.py
index 226ad85..806c400 100644
--- a/setup.py
+++ b/setup.py
@@ -66,6 +66,7 @@ def get_description():
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Topic :: Software Development :: Libraries :: Python Modules'
     ]
 )
diff --git a/tox.ini b/tox.ini
index 3d9526c..732cbe7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,6 +10,7 @@ deps=
     -r requirements/setup.txt
     -r requirements/test.txt
 commands=
+    {envbindir}/mypy --strict --show-error-codes --no-warn-unused-ignores wcmatch
     {envbindir}/py.test --cov wcmatch --cov-append tests
     {envbindir}/coverage html -d {envtmpdir}/coverage
     {envbindir}/coverage xml
diff --git a/wcmatch/__init__.py b/wcmatch/__init__.py
index fdfe7c5..352982b 100644
--- a/wcmatch/__init__.py
+++ b/wcmatch/__init__.py
@@ -21,5 +21,3 @@
 IN THE SOFTWARE.
 """
 from .__meta__ import __version_info__, __version__  # noqa: F401
-
-__all__ = tuple()
diff --git a/wcmatch/__meta__.py b/wcmatch/__meta__.py
index b81bd65..307785c 100644
--- a/wcmatch/__meta__.py
+++ b/wcmatch/__meta__.py
@@ -80,7 +80,11 @@ class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre"
 
     """
 
-    def __new__(cls, major, minor, micro, release="final", pre=0, post=0, dev=0):
+    def __new__(
+        cls,
+        major: int, minor: int, micro: int, release: str = "final",
+        pre: int = 0, post: int = 0, dev: int = 0
+    ) -> "Version":
         """Validate version info."""
 
         # Ensure all parts are positive integers.
@@ -116,27 +120,27 @@ def __new__(cls, major, minor, micro, release="final", pre=0, post=0, dev=0):
 
         return super(Version, cls).__new__(cls, major, minor, micro, release, pre, post, dev)
 
-    def _is_pre(self):
+    def _is_pre(self) -> bool:
         """Is prerelease."""
 
-        return self.pre > 0
+        return bool(self.pre > 0)
 
-    def _is_dev(self):
+    def _is_dev(self) -> bool:
         """Is development."""
 
         return bool(self.release < "alpha")
 
-    def _is_post(self):
+    def _is_post(self) -> bool:
         """Is post."""
 
-        return self.post > 0
+        return bool(self.post > 0)
 
-    def _get_dev_status(self):  # pragma: no cover
+    def _get_dev_status(self) -> str:  # pragma: no cover
         """Get development status string."""
 
         return DEV_STATUS[self.release]
 
-    def _get_canonical(self):
+    def _get_canonical(self) -> str:
         """Get the canonical output string."""
 
         # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
@@ -154,7 +158,7 @@ def _get_canonical(self):
         return ver
 
 
-def parse_version(ver):
+def parse_version(ver: str) -> Version:
     """Parse version into a comparable Version tuple."""
 
     m = RE_VER.match(ver)
@@ -189,5 +193,5 @@ def parse_version(ver):
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(8, 2, 0, "final")
+__version_info__ = Version(8, 3, 0, "final")
 __version__ = __version_info__._get_canonical()
diff --git a/wcmatch/_wcmatch.py b/wcmatch/_wcmatch.py
index b7757eb..0393102 100644
--- a/wcmatch/_wcmatch.py
+++ b/wcmatch/_wcmatch.py
@@ -4,6 +4,7 @@
 import stat
 import copyreg
 from . import util
+from typing import Pattern, Tuple, AnyStr, Optional, Generic, Any, Dict, cast
 
 # `O_DIRECTORY` may not always be defined
 DIR_FLAGS = os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0)
@@ -21,22 +22,39 @@
 )
 
 
-class _Match:
+class _Match(Generic[AnyStr]):
     """Match the given pattern."""
 
-    def __init__(self, filename, include, exclude, real, path, follow):
+    def __init__(
+        self,
+        filename: AnyStr,
+        include: Tuple[Pattern[AnyStr], ...],
+        exclude: Optional[Tuple[Pattern[AnyStr], ...]],
+        real: bool,
+        path: bool,
+        follow: bool
+    ) -> None:
         """Initialize."""
 
-        self.filename = filename
-        self.include = include
-        self.exclude = exclude
+        self.filename = filename  # type: AnyStr
+        self.include = include  # type: Tuple[Pattern[AnyStr], ...]
+        self.exclude = exclude  # type: Optional[Tuple[Pattern[AnyStr], ...]]
         self.real = real
         self.path = path
         self.follow = follow
-        self.is_bytes = isinstance(self.filename, bytes)
-        self.ptype = util.BYTES if self.is_bytes else util.UNICODE
-
-    def _fs_match(self, pattern, filename, is_dir, sep, follow, symlinks, root, dir_fd):
+        self.ptype = util.BYTES if isinstance(self.filename, bytes) else util.UNICODE
+
+    def _fs_match(
+        self,
+        pattern: Pattern[AnyStr],
+        filename: AnyStr,
+        is_dir: bool,
+        sep: AnyStr,
+        follow: bool,
+        symlinks: Dict[Tuple[Optional[int], AnyStr], bool],
+        root: AnyStr,
+        dir_fd: Optional[int]
+    ) -> bool:
         """
         Match path against the pattern.
 
@@ -98,12 +116,19 @@ def _fs_match(self, pattern, filename, is_dir, sep, follow, symlinks, root, dir_
                     matched = False
         return matched
 
-    def _match_real(self, symlinks, root, dir_fd):
+    def _match_real(
+        self,
+        symlinks: Dict[Tuple[Optional[int], AnyStr], bool],
+        root: AnyStr,
+        dir_fd: Optional[int]
+    ) -> bool:
         """Match real filename includes and excludes."""
 
-        sep = '\\' if util.platform() == "windows" else '/'
+        temp = '\\' if util.platform() == "windows" else '/'
         if isinstance(self.filename, bytes):
-            sep = os.fsencode(sep)
+            sep = os.fsencode(temp)
+        else:
+            sep = temp
 
         is_dir = self.filename.endswith(sep)
         try:
@@ -140,11 +165,14 @@ def _match_real(self, symlinks, root, dir_fd):
 
         return matched
 
-    def match(self, root_dir=None, dir_fd=None):
+    def match(self, root_dir: Optional[AnyStr] = None, dir_fd: Optional[int] = None) -> bool:
         """Match."""
 
         if self.real:
-            root = root_dir if root_dir else (b'.' if self.is_bytes else '.')
+            if isinstance(self.filename, bytes):
+                root = root_dir if root_dir is not None else b'.'  # type: AnyStr
+            else:
+                root = root_dir if root_dir is not None else '.'
 
             if dir_fd is not None and not SUPPORT_DIR_FD:
                 dir_fd = None
@@ -163,9 +191,8 @@ def match(self, root_dir=None, dir_fd=None):
                     )
                 )
 
-            is_abs = (
-                RE_WIN_MOUNT if util.platform() == "windows" else RE_MOUNT
-            )[self.ptype].match(self.filename) is not None
+            re_mount = cast(Pattern[AnyStr], (RE_WIN_MOUNT if util.platform() == "windows" else RE_MOUNT)[self.ptype])
+            is_abs = re_mount.match(self.filename) is not None
 
             if is_abs:
                 exists = os.path.lexists(self.filename)
@@ -180,7 +207,7 @@ def match(self, root_dir=None, dir_fd=None):
                     exists = True
 
             if exists:
-                symlinks = {}
+                symlinks = {}  # type: Dict[Tuple[Optional[int], AnyStr], bool]
                 return self._match_real(symlinks, root, dir_fd)
             else:
                 return False
@@ -201,15 +228,29 @@ def match(self, root_dir=None, dir_fd=None):
         return matched
 
 
-class WcRegexp(util.Immutable):
+class WcRegexp(util.Immutable, Generic[AnyStr]):
     """File name match object."""
 
+    _include: Tuple[Pattern[AnyStr], ...]
+    _exclude: Optional[Tuple[Pattern[AnyStr], ...]]
+    _real: bool
+    _path: bool
+    _follow: bool
+    _hash: int
+
     __slots__ = ("_include", "_exclude", "_real", "_path", "_follow", "_hash")
 
-    def __init__(self, include, exclude=None, real=False, path=False, follow=False):
+    def __init__(
+        self,
+        include: Tuple[Pattern[AnyStr], ...],
+        exclude: Optional[Tuple[Pattern[AnyStr], ...]] = None,
+        real: bool = False,
+        path: bool = False,
+        follow: bool = False
+    ):
         """Initialization."""
 
-        super(WcRegexp, self).__init__(
+        super().__init__(
             _include=include,
             _exclude=exclude,
             _real=real,
@@ -227,17 +268,17 @@ def __init__(self, include, exclude=None, real=False, path=False, follow=False):
             )
         )
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         """Hash."""
 
         return self._hash
 
-    def __len__(self):
+    def __len__(self) -> int:
         """Length."""
 
         return len(self._include) + (len(self._exclude) if self._exclude is not None else 0)
 
-    def __eq__(self, other):
+    def __eq__(self, other: Any) -> bool:
         """Equal."""
 
         return (
@@ -249,7 +290,7 @@ def __eq__(self, other):
             self._follow == other._follow
         )
 
-    def __ne__(self, other):
+    def __ne__(self, other: Any) -> bool:
         """Equal."""
 
         return (
@@ -261,7 +302,7 @@ def __ne__(self, other):
             self._follow != other._follow
         )
 
-    def match(self, filename, root_dir=None, dir_fd=None):
+    def match(self, filename: AnyStr, root_dir: Optional[AnyStr] = None, dir_fd: Optional[int] = None) -> bool:
         """Match filename."""
 
         return _Match(
@@ -277,7 +318,7 @@ def match(self, filename, root_dir=None, dir_fd=None):
         )
 
 
-def _pickle(p):
+def _pickle(p):  # type: ignore[no-untyped-def]
     return WcRegexp, (p._include, p._exclude, p._real, p._path, p._follow)
 
 
diff --git a/wcmatch/_wcparse.py b/wcmatch/_wcparse.py
index 4b7a1cf..930ed7a 100644
--- a/wcmatch/_wcparse.py
+++ b/wcmatch/_wcparse.py
@@ -27,6 +27,7 @@
 from . import util
 from . import posix
 from . _wcmatch import WcRegexp
+from typing import List, Tuple, AnyStr, Iterable, Pattern, Generic, Optional, Set, Sequence, Union, cast
 
 UNICODE_RANGE = '\u0000-\U0010ffff'
 ASCII_RANGE = '\x00-\xff'
@@ -298,7 +299,16 @@ class PatternLimitException(Exception):
     """Pattern limit exception."""
 
 
-def escape(pattern, unix=None, pathname=True, raw=False):
+def to_str_sequence(patterns: Union[str, bytes, Sequence[AnyStr]]) -> Sequence[AnyStr]:
+    """Return a simple string sequence."""
+
+    if isinstance(patterns, (str, bytes)):
+        return cast(Sequence[AnyStr], [patterns])
+    else:
+        return patterns
+
+
+def escape(pattern: AnyStr, unix: Optional[bool] = None, pathname: bool = True, raw: bool = False) -> AnyStr:
     """
     Escape.
 
@@ -309,17 +319,17 @@ def escape(pattern, unix=None, pathname=True, raw=False):
     """
 
     if isinstance(pattern, bytes):
-        drive_pat = RE_WIN_DRIVE[util.BYTES]
-        magic = RE_MAGIC_ESCAPE[util.BYTES]
-        drive_magic = RE_WIN_DRIVE_MAGIC[util.BYTES]
+        drive_pat = cast(Pattern[AnyStr], RE_WIN_DRIVE[util.BYTES])
+        magic = cast(Pattern[AnyStr], RE_MAGIC_ESCAPE[util.BYTES])
+        drive_magic = cast(Pattern[AnyStr], RE_WIN_DRIVE_MAGIC[util.BYTES])
         replace = br'\\\1'
         slash = b'\\'
         double_slash = b'\\\\'
         drive = b''
     else:
-        drive_pat = RE_WIN_DRIVE[util.UNICODE]
-        magic = RE_MAGIC_ESCAPE[util.UNICODE]
-        drive_magic = RE_WIN_DRIVE_MAGIC[util.UNICODE]
+        drive_pat = cast(Pattern[AnyStr], RE_WIN_DRIVE[util.UNICODE])
+        magic = cast(Pattern[AnyStr], RE_MAGIC_ESCAPE[util.UNICODE])
+        drive_magic = cast(Pattern[AnyStr], RE_WIN_DRIVE_MAGIC[util.UNICODE])
         replace = r'\\\1'
         slash = '\\'
         double_slash = '\\\\'
@@ -345,7 +355,11 @@ def escape(pattern, unix=None, pathname=True, raw=False):
     return drive + magic.sub(replace, pattern)
 
 
-def _get_win_drive(pattern, regex=False, case_sensitive=False):
+def _get_win_drive(
+    pattern: str,
+    regex: bool = False,
+    case_sensitive: bool = False
+) -> Tuple[bool, Optional[str], bool, int]:
     """Get Windows drive."""
 
     drive = None
@@ -392,47 +406,56 @@ def _get_win_drive(pattern, regex=False, case_sensitive=False):
     return root_specified, drive, slash, end
 
 
-def _get_magic_symbols(ptype, unix, flags):
+def _get_magic_symbols(pattern: AnyStr, unix: bool, flags: int) -> Tuple[Set[AnyStr], Set[AnyStr]]:
     """Get magic symbols."""
 
-    if ptype == util.BYTES:
-        slash = b'\\'
+    if isinstance(pattern, bytes):
+        ptype = util.BYTES
+        slash = b'\\'  # type: AnyStr
     else:
+        ptype = util.UNICODE
         slash = '\\'
 
-    magic = set()
-    magic_drive = set() if unix else set(slash)
+    magic = set()  # type: Set[AnyStr]
+    if unix:
+        magic_drive = set()  # type: Set[AnyStr]
+    else:
+        magic_drive = set([slash])
 
-    magic |= MAGIC_DEF[ptype]
+    magic |= cast(Set[AnyStr], MAGIC_DEF[ptype])
     if flags & BRACE:
-        magic |= MAGIC_BRACE[ptype]
-        magic_drive |= MAGIC_BRACE[ptype]
+        magic |= cast(Set[AnyStr], MAGIC_BRACE[ptype])
+        magic_drive |= cast(Set[AnyStr], MAGIC_BRACE[ptype])
     if flags & SPLIT:
-        magic |= MAGIC_SPLIT[ptype]
-        magic_drive |= MAGIC_SPLIT[ptype]
+        magic |= cast(Set[AnyStr], MAGIC_SPLIT[ptype])
+        magic_drive |= cast(Set[AnyStr], MAGIC_SPLIT[ptype])
     if flags & GLOBTILDE:
-        magic |= MAGIC_TILDE[ptype]
+        magic |= cast(Set[AnyStr], MAGIC_TILDE[ptype])
     if flags & EXTMATCH:
-        magic |= MAGIC_EXTMATCH[ptype]
+        magic |= cast(Set[AnyStr], MAGIC_EXTMATCH[ptype])
     if flags & NEGATE:
         if flags & MINUSNEGATE:
-            magic |= MAGIC_MINUS_NEGATE[ptype]
+            magic |= cast(Set[AnyStr], MAGIC_MINUS_NEGATE[ptype])
         else:
-            magic |= MAGIC_NEGATE[ptype]
+            magic |= cast(Set[AnyStr], MAGIC_NEGATE[ptype])
 
     return magic, magic_drive
 
 
-def is_magic(pattern, flags=0):
+def is_magic(pattern: AnyStr, flags: int = 0) -> bool:
     """Check if pattern is magic."""
 
     magical = False
     unix = is_unix_style(flags)
 
-    ptype = util.BYTES if isinstance(pattern, bytes) else util.UNICODE
-    drive_pat = RE_WIN_DRIVE[ptype]
+    if isinstance(pattern, bytes):
+        ptype = util.BYTES
+    else:
+        ptype = util.UNICODE
+
+    drive_pat = cast(Pattern[AnyStr], RE_WIN_DRIVE[ptype])
 
-    magic, magic_drive = _get_magic_symbols(ptype, unix, flags)
+    magic, magic_drive = _get_magic_symbols(pattern, unix, flags)
     is_path = flags & PATHNAME
 
     length = 0
@@ -456,18 +479,18 @@ def is_magic(pattern, flags=0):
     return magical
 
 
-def is_negative(pattern, flags):
+def is_negative(pattern: AnyStr, flags: int) -> bool:
     """Check if negative pattern."""
 
     if flags & MINUSNEGATE:
-        return flags & NEGATE and pattern[0:1] in MINUS_NEGATIVE_SYM
+        return bool(flags & NEGATE and pattern[0:1] in MINUS_NEGATIVE_SYM)
     elif flags & EXTMATCH:
-        return flags & NEGATE and pattern[0:1] in NEGATIVE_SYM and pattern[1:2] not in ROUND_BRACKET
+        return bool(flags & NEGATE and pattern[0:1] in NEGATIVE_SYM and pattern[1:2] not in ROUND_BRACKET)
     else:
-        return flags & NEGATE and pattern[0:1] in NEGATIVE_SYM
+        return bool(flags & NEGATE and pattern[0:1] in NEGATIVE_SYM)
 
 
-def tilde_pos(pattern, flags):
+def tilde_pos(pattern: AnyStr, flags: int) -> int:
     """Is user folder."""
 
     pos = -1
@@ -482,7 +505,7 @@ def tilde_pos(pattern, flags):
     return pos
 
 
-def expand_braces(patterns, flags, limit):
+def expand_braces(patterns: AnyStr, flags: int, limit: int) -> Iterable[AnyStr]:
     """Expand braces."""
 
     if flags & BRACE:
@@ -502,24 +525,24 @@ def expand_braces(patterns, flags, limit):
             yield p
 
 
-def expand_tilde(pattern, is_unix, flags):
+def expand_tilde(pattern: AnyStr, is_unix: bool, flags: int) -> AnyStr:
     """Expand tilde."""
 
     pos = tilde_pos(pattern, flags)
 
     if pos > -1:
         string_type = util.BYTES if isinstance(pattern, bytes) else util.UNICODE
-        tilde = TILDE_SYM[string_type]
-        re_tilde = RE_WIN_TILDE[string_type] if not is_unix else RE_TILDE[string_type]
+        tilde = cast(AnyStr, TILDE_SYM[string_type])
+        re_tilde = cast(Pattern[AnyStr], RE_WIN_TILDE[string_type] if not is_unix else RE_TILDE[string_type])
         m = re_tilde.match(pattern, pos)
         if m:
             expanded = os.path.expanduser(m.group(0))
             if not expanded.startswith(tilde) and os.path.exists(expanded):
-                pattern = (pattern[0] if pos else pattern[0:0]) + escape(expanded, is_unix) + pattern[m.end(0):]
+                pattern = (pattern[0:1] if pos else pattern[0:0]) + escape(expanded, is_unix) + pattern[m.end(0):]
     return pattern
 
 
-def expand(pattern, flags, limit):
+def expand(pattern: AnyStr, flags: int, limit: int) -> Iterable[AnyStr]:
     """Expand and normalize."""
 
     for expanded in expand_braces(pattern, flags, limit):
@@ -527,7 +550,7 @@ def expand(pattern, flags, limit):
             yield expand_tilde(splitted, is_unix_style(flags), flags)
 
 
-def is_case_sensitive(flags):
+def is_case_sensitive(flags: int) -> bool:
     """Is case sensitive."""
 
     if bool(flags & FORCEWIN):
@@ -539,7 +562,7 @@ def is_case_sensitive(flags):
     return case_sensitive
 
 
-def get_case(flags):
+def get_case(flags: int) -> bool:
     """Parse flags for case sensitivity settings."""
 
     if not bool(flags & CASE_FLAGS):
@@ -551,13 +574,13 @@ def get_case(flags):
     return case_sensitive
 
 
-def escape_drive(drive, case):
+def escape_drive(drive: str, case: bool) -> str:
     """Escape drive."""
 
     return '(?i:{})'.format(re.escape(drive)) if case else re.escape(drive)
 
 
-def is_unix_style(flags):
+def is_unix_style(flags: int) -> bool:
     """Check if we should use Unix style."""
 
     return (
@@ -569,13 +592,15 @@ def is_unix_style(flags):
     )
 
 
-def translate(patterns, flags, limit=PATTERN_LIMIT):
+def translate(
+    patterns: Sequence[AnyStr],
+    flags: int,
+    limit: int = PATTERN_LIMIT
+) -> Tuple[List[AnyStr], List[AnyStr]]:
     """Translate patterns."""
 
-    positive = []
-    negative = []
-    if isinstance(patterns, (str, bytes)):
-        patterns = [patterns]
+    positive = []  # type: List[AnyStr]
+    negative = []  # type: List[AnyStr]
 
     flags = (flags | _TRANSLATE) & FLAG_MASK
     is_unix = is_unix_style(flags)
@@ -585,7 +610,7 @@ def translate(patterns, flags, limit=PATTERN_LIMIT):
         current_limit = limit
         total = 0
         for pattern in patterns:
-            pattern = util.norm_pattern(pattern, not is_unix, flags & RAWCHARS)
+            pattern = util.norm_pattern(pattern, not is_unix, bool(flags & RAWCHARS))
             count = 0
             for count, expanded in enumerate(expand(pattern, flags, current_limit), 1):
                 total += 1
@@ -601,20 +626,22 @@ def translate(patterns, flags, limit=PATTERN_LIMIT):
     except bracex.ExpansionLimitException:
         raise PatternLimitException("Pattern limit exceeded the limit of {:d}".format(limit))
 
-    if patterns and negative and not positive:
+    if patterns is not None and negative and not positive:
         if flags & NEGATEALL:
             default = b'**' if isinstance(patterns[0], bytes) else '**'
-            positive.append(WcParse(default, flags | (GLOBSTAR if flags & PATHNAME else 0)).parse())
+            positive.append(
+                WcParse(default, flags | (GLOBSTAR if flags & PATHNAME else 0)).parse()
+            )
 
     if patterns and flags & NODIR:
         index = util.BYTES if isinstance(patterns[0], bytes) else util.UNICODE
-        exclude = _NO_NIX_DIR[index] if is_unix else _NO_WIN_DIR[index]
+        exclude = cast(AnyStr, _NO_NIX_DIR[index] if is_unix else _NO_WIN_DIR[index])
         negative.append(exclude)
 
     return positive, negative
 
 
-def split(pattern, flags):
+def split(pattern: AnyStr, flags: int) -> Iterable[AnyStr]:
     """Split patterns."""
 
     if flags & SPLIT:
@@ -623,13 +650,15 @@ def split(pattern, flags):
         yield pattern
 
 
-def compile(patterns, flags, limit=PATTERN_LIMIT):  # noqa A001
+def compile(  # noqa: A001
+    patterns: Sequence[AnyStr],
+    flags: int,
+    limit: int = PATTERN_LIMIT
+) -> WcRegexp[AnyStr]:
     """Compile patterns."""
 
-    positive = []
-    negative = []
-    if isinstance(patterns, (str, bytes)):
-        patterns = [patterns]
+    positive = []  # type: List[Pattern[AnyStr]]
+    negative = []  # type: List[Pattern[AnyStr]]
 
     is_unix = is_unix_style(flags)
     seen = set()
@@ -638,7 +667,7 @@ def compile(patterns, flags, limit=PATTERN_LIMIT):  # noqa A001
         current_limit = limit
         total = 0
         for pattern in patterns:
-            pattern = util.norm_pattern(pattern, not is_unix, flags & RAWCHARS)
+            pattern = util.norm_pattern(pattern, not is_unix, bool(flags & RAWCHARS))
             count = 0
             for count, expanded in enumerate(expand(pattern, flags, current_limit), 1):
                 total += 1
@@ -654,39 +683,41 @@ def compile(patterns, flags, limit=PATTERN_LIMIT):  # noqa A001
     except bracex.ExpansionLimitException:
         raise PatternLimitException("Pattern limit exceeded the limit of {:d}".format(limit))
 
-    if patterns and negative and not positive:
+    if patterns is not None and negative and not positive:
         if flags & NEGATEALL:
             default = b'**' if isinstance(patterns[0], bytes) else '**'
             positive.append(_compile(default, flags | (GLOBSTAR if flags & PATHNAME else 0)))
 
-    if patterns and flags & NODIR:
+    if patterns is not None and flags & NODIR:
         ptype = util.BYTES if isinstance(patterns[0], bytes) else util.UNICODE
-        negative.append(RE_NO_DIR[ptype] if is_unix else RE_WIN_NO_DIR[ptype])
+        negative.append(cast(Pattern[AnyStr], RE_NO_DIR[ptype] if is_unix else RE_WIN_NO_DIR[ptype]))
 
-    return WcRegexp(tuple(positive), tuple(negative), flags & REALPATH, flags & PATHNAME, flags & FOLLOW)
+    return WcRegexp(
+        tuple(positive), tuple(negative),
+        bool(flags & REALPATH), bool(flags & PATHNAME), bool(flags & FOLLOW)
+    )
 
 
 @functools.lru_cache(maxsize=256, typed=True)
-def _compile(pattern, flags):
+def _compile(pattern: AnyStr, flags: int) -> Pattern[AnyStr]:
     """Compile the pattern to regex."""
 
     return re.compile(WcParse(pattern, flags & FLAG_MASK).parse())
 
 
-class WcSplit(object):
+class WcSplit(Generic[AnyStr]):
     """Class that splits patterns on |."""
 
-    def __init__(self, pattern, flags):
+    def __init__(self, pattern: AnyStr, flags: int) -> None:
         """Initialize."""
 
-        self.pattern = pattern
-        self.is_bytes = isinstance(pattern, bytes)
+        self.pattern = pattern  # type: AnyStr
         self.pathname = bool(flags & PATHNAME)
         self.extend = bool(flags & EXTMATCH)
         self.unix = is_unix_style(flags)
         self.bslash_abort = not self.unix
 
-    def _sequence(self, i):
+    def _sequence(self, i: util.StringIter) -> None:
         """Handle character group."""
 
         c = next(i)
@@ -707,7 +738,7 @@ def _sequence(self, i):
                     raise StopIteration
             c = next(i)
 
-    def _references(self, i, sequence=False):
+    def _references(self, i: util.StringIter, sequence: bool = False) -> None:
         """Handle references."""
 
         c = next(i)
@@ -723,7 +754,7 @@ def _references(self, i, sequence=False):
             # \a, \b, \c, etc.
             pass
 
-    def parse_extend(self, c, i):
+    def parse_extend(self, c: str, i: util.StringIter) -> bool:
         """Parse extended pattern lists."""
 
         # Start list parsing
@@ -759,14 +790,12 @@ def parse_extend(self, c, i):
 
         return success
 
-    def split(self):
-        """Start parsing the pattern."""
-
-        pattern = self.pattern.decode('latin-1') if self.is_bytes else self.pattern
+    def _split(self, pattern: str) -> Iterable[str]:
+        """Split the pattern."""
 
         start = -1
         i = util.StringIter(pattern)
-        iter(i)
+
         for c in i:
             if self.extend and c in EXT_TYPES and self.parse_extend(c, i):
                 continue
@@ -774,7 +803,7 @@ def split(self):
             if c == '|':
                 split = i.index - 1
                 p = pattern[start + 1:split]
-                yield p.encode('latin-1') if self.is_bytes else p
+                yield p
                 start = split
             elif c == '\\':
                 index = i.index
@@ -790,17 +819,25 @@ def split(self):
                     i.rewind(i.index - index)
 
         if start < len(pattern):
-            p = pattern[start + 1:]
-            yield p.encode('latin-1') if self.is_bytes else p
+            yield pattern[start + 1:]
+
+    def split(self) -> Iterable[AnyStr]:
+        """Split the pattern."""
+
+        if isinstance(self.pattern, bytes):
+            for p in self._split(self.pattern.decode('latin-1')):
+                yield p.encode('latin-1')
+        else:
+            yield from self._split(self.pattern)
 
 
-class WcParse(object):
+class WcParse(Generic[AnyStr]):
     """Parse the wildcard pattern."""
 
-    def __init__(self, pattern, flags=0):
+    def __init__(self, pattern: AnyStr, flags: int = 0) -> None:
         """Initialize."""
 
-        self.pattern = pattern
+        self.pattern = pattern  # type: AnyStr
         self.no_abs = bool(flags & _NOABSOLUTE)
         self.braces = bool(flags & BRACE)
         self.is_bytes = isinstance(pattern, bytes)
@@ -827,7 +864,7 @@ def __init__(self, pattern, flags=0):
         self.unix = is_unix_style(self.flags)
         if not self.unix:
             self.win_drive_detect = self.pathname
-            self.char_avoid = (ord('\\'), ord('/'), ord('.'))
+            self.char_avoid = (ord('\\'), ord('/'), ord('.'))  # type: Tuple[int, ...]
             self.bslash_abort = self.pathname
             sep = {"sep": re.escape('\\/')}
         else:
@@ -851,25 +888,25 @@ def __init__(self, pattern, flags=0):
         else:
             self.need_char = _NEED_CHAR
 
-    def set_after_start(self):
+    def set_after_start(self) -> None:
         """Set tracker for character after the start of a directory."""
 
         self.after_start = True
         self.dir_start = False
 
-    def set_start_dir(self):
+    def set_start_dir(self) -> None:
         """Set directory start."""
 
         self.dir_start = True
         self.after_start = False
 
-    def reset_dir_track(self):
+    def reset_dir_track(self) -> None:
         """Reset directory tracker."""
 
         self.dir_start = False
         self.after_start = False
 
-    def update_dir_state(self):
+    def update_dir_state(self) -> None:
         """
         Update the directory state.
 
@@ -883,12 +920,12 @@ def update_dir_state(self):
         elif not self.dir_start and self.after_start:
             self.reset_dir_track()
 
-    def _restrict_extended_slash(self):
+    def _restrict_extended_slash(self) -> str:
         """Restrict extended slash."""
 
         return self.seq_path if self.pathname else ''
 
-    def _restrict_sequence(self):
+    def _restrict_sequence(self) -> str:
         """Restrict sequence."""
 
         if self.pathname:
@@ -901,7 +938,7 @@ def _restrict_sequence(self):
 
         return value
 
-    def _sequence_range_check(self, result, last):
+    def _sequence_range_check(self, result: List[str], last: str) -> bool:
         """
         If range backwards, remove it.
 
@@ -926,7 +963,7 @@ def _sequence_range_check(self, result, last):
             result.append(last)
         return removed
 
-    def _handle_posix(self, i, result, end_range):
+    def _handle_posix(self, i: util.StringIter, result: List[str], end_range: int) -> bool:
         """Handle posix classes."""
 
         last_posix = False
@@ -941,7 +978,7 @@ def _handle_posix(self, i, result, end_range):
             result.append(posix.get_posix_property(m.group(1), self.is_bytes))
         return last_posix
 
-    def _sequence(self, i):
+    def _sequence(self, i: util.StringIter) -> str:
         """Handle character group."""
 
         result = ['[']
@@ -1043,7 +1080,7 @@ def _sequence(self, i):
 
         return ''.join(result)
 
-    def _references(self, i, sequence=False):
+    def _references(self, i: util.StringIter, sequence: bool = False) -> str:
         """Handle references."""
 
         value = ''
@@ -1083,7 +1120,7 @@ def _references(self, i, sequence=False):
 
         return value
 
-    def _handle_dot(self, i, current):
+    def _handle_dot(self, i: util.StringIter, current: List[str]) -> None:
         """Handle dot."""
 
         is_current = True
@@ -1133,7 +1170,7 @@ def _handle_dot(self, i, current):
         else:
             current.append(re.escape('.'))
 
-    def _handle_star(self, i, current):
+    def _handle_star(self, i: util.StringIter, current: List[str]) -> None:
         """Handle star."""
 
         if self.pathname:
@@ -1227,7 +1264,7 @@ def _handle_star(self, i, current):
         else:
             current.append(value)
 
-    def clean_up_inverse(self, current, nested=False):
+    def clean_up_inverse(self, current: List[str], nested: bool = False) -> None:
         """
         Clean up current.
 
@@ -1258,7 +1295,7 @@ def clean_up_inverse(self, current, nested=False):
             index -= 1
         self.inv_ext = 0
 
-    def parse_extend(self, c, i, current, reset_dot=False):
+    def parse_extend(self, c: str, i: util.StringIter, current: List[str], reset_dot: bool = False) -> bool:
         """Parse extended pattern lists."""
 
         # Save state
@@ -1277,7 +1314,7 @@ def parse_extend(self, c, i, current, reset_dot=False):
         success = True
         index = i.index
         list_type = c
-        extended = []
+        extended = []  # type: List[str]
 
         try:
             c = next(i)
@@ -1382,7 +1419,7 @@ def parse_extend(self, c, i, current, reset_dot=False):
 
         return success
 
-    def consume_path_sep(self, i):
+    def consume_path_sep(self, i: util.StringIter) -> None:
         """Consume any consecutive path separators as they count as one."""
 
         try:
@@ -1407,12 +1444,12 @@ def consume_path_sep(self, i):
         except StopIteration:
             pass
 
-    def root(self, pattern, current):
+    def root(self, pattern: str, current: List[str]) -> None:
         """Start parsing the pattern."""
 
         self.set_after_start()
         i = util.StringIter(pattern)
-        iter(i)
+
         root_specified = False
         if self.win_drive_detect:
             root_specified, drive, slash, end = _get_win_drive(pattern, True, self.case_sensitive)
@@ -1491,21 +1528,18 @@ def root(self, pattern, current):
         if self.pathname:
             current.append(_PATH_TRAIL.format(self.sep))
 
-    def parse(self):
-        """Parse pattern list."""
+    def _parse(self, p: str) -> str:
+        """Parse pattern."""
 
         result = ['']
         prepend = ['']
-        self.negative = False
 
-        p = self.pattern
+        self.negative = False
 
         if is_negative(p, self.flags):
             self.negative = True
             p = p[1:]
 
-        p = p.decode('latin-1') if self.is_bytes else p
-
         if self.negative:
             # TODO: Do we prevent `NODOTDIR` for negative patterns?
             self.globstar_capture = False
@@ -1559,7 +1593,14 @@ def parse(self):
             # Strip out unnecessary regex comments
             pattern = pattern.replace('(?#)', '')
 
-        if self.is_bytes:
-            pattern = pattern.encode('latin-1')
+        return pattern
+
+    def parse(self) -> AnyStr:
+        """Parse pattern list."""
+
+        if isinstance(self.pattern, bytes):
+            pattern = self._parse(self.pattern.decode('latin-1')).encode('latin-1')
+        else:
+            pattern = self._parse(self.pattern)
 
         return pattern
diff --git a/wcmatch/fnmatch.py b/wcmatch/fnmatch.py
index 21d2be6..2d813d4 100644
--- a/wcmatch/fnmatch.py
+++ b/wcmatch/fnmatch.py
@@ -21,6 +21,7 @@
 IN THE SOFTWARE.
 """
 from . import _wcparse
+from typing import Tuple, List, AnyStr, Iterable, Union, Sequence
 
 __all__ = (
     "CASE", "EXTMATCH", "IGNORECASE", "RAWCHARS",
@@ -59,7 +60,7 @@
 )
 
 
-def _flag_transform(flags):
+def _flag_transform(flags: int) -> int:
     """Transform flags to glob defaults."""
 
     # Enabling both cancels out
@@ -69,14 +70,25 @@ def _flag_transform(flags):
     return (flags & FLAG_MASK)
 
 
-def translate(patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
+def translate(
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> Tuple[List[AnyStr], List[AnyStr]]:
     """Translate `fnmatch` pattern."""
 
     flags = _flag_transform(flags)
-    return _wcparse.translate(patterns, flags, limit)
+    return _wcparse.translate(_wcparse.to_str_sequence(patterns), flags, limit)
 
 
-def fnmatch(filename, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
+def fnmatch(
+    filename: AnyStr,
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> bool:
     """
     Check if filename matches pattern.
 
@@ -85,16 +97,22 @@ def fnmatch(filename, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
     """
 
     flags = _flag_transform(flags)
-    return _wcparse.compile(patterns, flags, limit).match(filename)
+    return bool(_wcparse.compile(_wcparse.to_str_sequence(patterns), flags, limit).match(filename))
 
 
-def filter(filenames, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):  # noqa A001
+def filter(  # noqa A001
+    filenames: Iterable[AnyStr],
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> List[AnyStr]:
     """Filter names using pattern."""
 
     matches = []
 
     flags = _flag_transform(flags)
-    obj = _wcparse.compile(patterns, flags, limit)
+    obj = _wcparse.compile(_wcparse.to_str_sequence(patterns), flags, limit)
 
     for filename in filenames:
         if obj.match(filename):
@@ -102,13 +120,13 @@ def filter(filenames, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):  # no
     return matches
 
 
-def escape(pattern):
+def escape(pattern: AnyStr) -> AnyStr:
     """Escape."""
 
     return _wcparse.escape(pattern, pathname=False)
 
 
-def is_magic(pattern, *, flags=0):
+def is_magic(pattern: AnyStr, *, flags: int = 0) -> bool:
     """Check if the pattern is likely to be magic."""
 
     flags = _flag_transform(flags)
diff --git a/wcmatch/glob.py b/wcmatch/glob.py
index 6d808ac..e92386f 100644
--- a/wcmatch/glob.py
+++ b/wcmatch/glob.py
@@ -25,9 +25,14 @@
 import re
 import functools
 from collections import namedtuple
+import bracex
 from . import _wcparse
 from . import _wcmatch
 from . import util
+from typing import (
+    Optional, Iterator, Iterable, List, AnyStr, Union, Generic,
+    Tuple, Pattern, Callable, Any, Set, Sequence, cast
+)
 
 __all__ = (
     "CASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH",
@@ -102,19 +107,18 @@
     _NOABSOLUTE
 )
 
-_RE_PATHLIB_DOT_NORM = [
+_RE_PATHLIB_DOT_NORM = (
     re.compile(r'(?:((?<=^)|(?<=/))\.(?:/|$))+'),
     re.compile(br'(?:((?<=^)|(?<=/))\.(?:/|$))+')
+)  # type: Tuple[Pattern[str], Pattern[bytes]]
 
-]
-
-_RE_WIN_PATHLIB_DOT_NORM = [
+_RE_WIN_PATHLIB_DOT_NORM = (
     re.compile(r'(?:((?<=^)|(?<=[\\/]))\.(?:[\\/]|$))+'),
     re.compile(br'(?:((?<=^)|(?<=[\\/]))\.(?:[\\/]|$))+')
-]
+)  # type: Tuple[Pattern[str], Pattern[bytes]]
 
 
-def _flag_transform(flags):
+def _flag_transform(flags: int) -> int:
     """Transform flags to glob defaults."""
 
     # Enabling both cancels out
@@ -135,11 +139,13 @@ def _flag_transform(flags):
     return flags
 
 
-class _GlobPart(namedtuple('_GlobPart', ['pattern', 'is_magic', 'is_globstar', 'dir_only', 'is_drive'])):
+class _GlobPart(
+    namedtuple('_GlobPart', ['pattern', 'is_magic', 'is_globstar', 'dir_only', 'is_drive']),
+):
     """File Glob."""
 
 
-class _GlobSplit(object):
+class _GlobSplit(Generic[AnyStr]):
     """
     Split glob pattern on "magic" file and directories.
 
@@ -166,12 +172,12 @@ class _GlobSplit(object):
 
     """
 
-    def __init__(self, pattern, flags):
+    def __init__(self, pattern: AnyStr, flags: int) -> None:
         """Initialize."""
 
+        self.pattern = pattern  # type: AnyStr
         self.unix = _wcparse.is_unix_style(flags)
         self.flags = flags
-        self.pattern = pattern
         self.no_abs = bool(flags & _wcparse._NOABSOLUTE)
         self.globstar = bool(flags & GLOBSTAR)
         self.matchbase = bool(flags & MATCHBASE)
@@ -185,7 +191,6 @@ def __init__(self, pattern, flags):
         if flags & NEGATE:
             flags ^= NEGATE
         self.flags = flags
-        self.is_bytes = isinstance(pattern, bytes)
         self.extend = bool(flags & EXTMATCH)
         if not self.unix:
             self.win_drive_detect = True
@@ -197,10 +202,9 @@ def __init__(self, pattern, flags):
             self.sep = '/'
         # Once split, Windows file names will never have `\\` in them,
         # so we can use the Unix magic detect
-        ptype = util.BYTES if self.is_bytes else util.UNICODE
-        self.magic_symbols = _wcparse._get_magic_symbols(ptype, self.unix, self.flags)[0]
+        self.magic_symbols = _wcparse._get_magic_symbols(pattern, self.unix, self.flags)[0]  # type: Set[AnyStr]
 
-    def is_magic(self, name):
+    def is_magic(self, name: AnyStr) -> bool:
         """Check if name contains magic characters."""
 
         for c in self.magic_symbols:
@@ -208,7 +212,7 @@ def is_magic(self, name):
                 return True
         return False
 
-    def _sequence(self, i):
+    def _sequence(self, i: util.StringIter) -> None:
         """Handle character group."""
 
         c = next(i)
@@ -228,7 +232,7 @@ def _sequence(self, i):
                 raise StopIteration
             c = next(i)
 
-    def _references(self, i, sequence=False):
+    def _references(self, i: util.StringIter, sequence: bool = False) -> str:
         """Handle references."""
 
         value = ''
@@ -249,7 +253,7 @@ def _references(self, i, sequence=False):
             pass
         return value
 
-    def parse_extend(self, c, i):
+    def parse_extend(self, c: str, i: util.StringIter) -> bool:
         """Parse extended pattern lists."""
 
         # Start list parsing
@@ -285,7 +289,7 @@ def parse_extend(self, c, i):
 
         return success
 
-    def store(self, value, l, dir_only):
+    def store(self, value: AnyStr, l: List[_GlobPart], dir_only: bool) -> None:
         """Group patterns by literals and potential magic patterns."""
 
         if l and value in (b'', ''):
@@ -294,39 +298,43 @@ def store(self, value, l, dir_only):
         globstar = value in (b'**', '**') and self.globstar
         magic = self.is_magic(value)
         if magic:
-            value = _wcparse._compile(value, self.flags)
+            v = cast(Pattern[AnyStr], _wcparse._compile(value, self.flags))  # type: Union[Pattern[AnyStr], AnyStr]
+        else:
+            v = value
         if globstar and l and l[-1].is_globstar:
-            l[-1] = _GlobPart(value, magic, globstar, dir_only, False)
+            l[-1] = _GlobPart(v, magic, globstar, dir_only, False)
         else:
-            l.append(_GlobPart(value, magic, globstar, dir_only, False))
+            l.append(_GlobPart(v, magic, globstar, dir_only, False))
 
-    def split(self):
+    def split(self) -> List[_GlobPart]:
         """Start parsing the pattern."""
 
         split_index = []
         parts = []
         start = -1
 
-        pattern = self.pattern.decode('latin-1') if self.is_bytes else self.pattern
+        if isinstance(self.pattern, bytes):
+            is_bytes = True
+            pattern = self.pattern.decode('latin-1')
+        else:
+            is_bytes = False
+            pattern = self.pattern
 
         i = util.StringIter(pattern)
-        iter(i)
 
         # Detect and store away windows drive as a literal
         if self.win_drive_detect:
             root_specified, drive, slash, end = _wcparse._get_win_drive(pattern)
             if drive is not None:
-                if self.is_bytes:
-                    drive = drive.encode('latin-1')
-                parts.append(_GlobPart(drive, False, False, True, True))
+                parts.append(_GlobPart(drive.encode('latin-1') if is_bytes else drive, False, False, True, True))
                 start = end - 1
                 i.advance(start)
             elif drive is None and root_specified:
-                parts.append(_GlobPart(b'\\' if self.is_bytes else '\\', False, False, True, True))
+                parts.append(_GlobPart(b'\\' if is_bytes else '\\', False, False, True, True))
                 start = 1
                 i.advance(2)
         elif not self.win_drive_detect and pattern.startswith('/'):
-            parts.append(_GlobPart(b'/' if self.is_bytes else '/', False, False, True, True))
+            parts.append(_GlobPart(b'/' if is_bytes else '/', False, False, True, True))
             start = 0
             i.advance(1)
 
@@ -353,30 +361,24 @@ def split(self):
                     i.rewind(i.index - index)
 
         for split, offset in split_index:
-            if self.is_bytes:
-                value = pattern[start + 1:split].encode('latin-1')
-            else:
-                value = pattern[start + 1:split]
-            self.store(value, parts, True)
+            value = pattern[start + 1:split]
+            self.store(cast(AnyStr, value.encode('latin-1') if is_bytes else value), parts, True)
             start = split + offset
 
         if start < len(pattern):
-            if self.is_bytes:
-                value = pattern[start + 1:].encode('latin-1')
-            else:
-                value = pattern[start + 1:]
+            value = pattern[start + 1:]
             if value:
-                self.store(value, parts, False)
+                self.store(cast(AnyStr, value.encode('latin-1') if is_bytes else value), parts, False)
 
         if len(pattern) == 0:
-            parts.append(_GlobPart(pattern.encode('latin-1') if self.is_bytes else pattern, False, False, False, False))
+            parts.append(_GlobPart(pattern.encode('latin-1') if is_bytes else pattern, False, False, False, False))
 
         if (
             (self.extmatchbase and not parts[0].is_drive) or
             (self.matchbase and len(parts) == 1 and not parts[0].dir_only)
         ):
             self.globstar = True
-            parts.insert(0, _GlobPart(b'**' if self.is_bytes else '**', True, True, True, False))
+            parts.insert(0, _GlobPart(b'**' if is_bytes else '**', True, True, True, False))
 
         if self.no_abs and parts and parts[0].is_drive:
             raise ValueError('The pattern must be a relative path pattern')
@@ -384,75 +386,92 @@ def split(self):
         return parts
 
 
-class Glob(object):
+class Glob(Generic[AnyStr]):
     """Glob patterns."""
 
-    def __init__(self, pattern, flags=0, root_dir=None, dir_fd=None, limit=_wcparse.PATTERN_LIMIT):
+    def __init__(
+        self,
+        pattern: Union[str, bytes, Sequence[AnyStr]],
+        flags: int = 0,
+        root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
+        dir_fd: Optional[int] = None,
+        limit: int = _wcparse.PATTERN_LIMIT
+    ) -> None:
         """Initialize the directory walker object."""
 
-        self.seen = set()
-        self.is_bytes = isinstance(pattern[0], bytes)
-        self.current = b'.' if self.is_bytes else '.'
-        self.dir_fd = dir_fd if SUPPORT_DIR_FD else None
-        self.root_dir = os.fspath(root_dir) if root_dir is not None else self.current
-        self.nounique = bool(flags & NOUNIQUE)
-        self.mark = bool(flags & MARK)
+        pats = _wcparse.to_str_sequence(pattern)
+
+        self.pattern = []  # type: List[List[_GlobPart]]
+        self.npatterns = []  # type: List[Pattern[AnyStr]]
+        self.seen = set()  # type: Set[AnyStr]
+        self.dir_fd = dir_fd if SUPPORT_DIR_FD else None  # type: Optional[int]
+        self.nounique = bool(flags & NOUNIQUE)  # type: bool
+        self.mark = bool(flags & MARK)  # type: bool
         # Only scan for `.` and `..` if it is specifically requested.
-        self.scandotdir = flags & SCANDOTDIR
+        self.scandotdir = bool(flags & SCANDOTDIR)  # type: bool
         if self.mark:
             flags ^= MARK
-        self.negateall = bool(flags & NEGATEALL)
+        self.negateall = bool(flags & NEGATEALL)  # type: bool
         if self.negateall:
             flags ^= NEGATEALL
-        self.nodir = bool(flags & NODIR)
+        self.nodir = bool(flags & NODIR)  # type: bool
         if self.nodir:
             flags ^= NODIR
-        self.pathlib = bool(flags & _PATHLIB)
+        self.pathlib = bool(flags & _PATHLIB)  # type: bool
         if self.pathlib:
             flags ^= _PATHLIB
         # Right to left searching is only for matching
         if flags & _RTL:  # pragma: no cover
             flags ^= _RTL
-        self.flags = _flag_transform(flags | REALPATH)
-        self.negate_flags = self.flags
+        self.flags = _flag_transform(flags | REALPATH)  # type: int
+        self.negate_flags = self.flags  # type: int
         if not self.scandotdir and not self.flags & NODOTDIR:
             self.flags |= NODOTDIR
-        self.raw_chars = bool(self.flags & RAWCHARS)
-        self.follow_links = bool(self.flags & FOLLOW)
-        self.dot = bool(self.flags & DOTMATCH)
-        self.unix = not bool(self.flags & FORCEWIN)
-        self.negate = bool(self.flags & NEGATE)
-        self.globstar = bool(self.flags & GLOBSTAR)
-        self.braces = bool(self.flags & BRACE)
-        self.matchbase = bool(self.flags & MATCHBASE)
-        self.case_sensitive = _wcparse.get_case(self.flags)
-        self.specials = (b'.', b'..') if self.is_bytes else ('.', '..')
-        self.empty = b'' if self.is_bytes else ''
-        self.stars = b'**' if self.is_bytes else '**'
-        self.limit = limit
-        if self.flags & FORCEWIN:
-            self.sep = b'\\' if self.is_bytes else '\\'
-            self.seps = (b'/' if self.is_bytes else '/', self.sep)
-            self.re_pathlib_norm = _RE_WIN_PATHLIB_DOT_NORM[util.BYTES if self.is_bytes else util.UNICODE]
-            self.re_no_dir = _wcparse.RE_WIN_NO_DIR[util.BYTES if self.is_bytes else util.UNICODE]
+        self.raw_chars = bool(self.flags & RAWCHARS)  # type: bool
+        self.follow_links = bool(self.flags & FOLLOW)  # type: bool
+        self.dot = bool(self.flags & DOTMATCH)  # type: bool
+        self.unix = not bool(self.flags & FORCEWIN)  # type: bool
+        self.negate = bool(self.flags & NEGATE)  # type: bool
+        self.globstar = bool(self.flags & GLOBSTAR)  # type: bool
+        self.braces = bool(self.flags & BRACE)  # type: bool
+        self.matchbase = bool(self.flags & MATCHBASE)  # type: bool
+        self.case_sensitive = _wcparse.get_case(self.flags)  # type: bool
+        self.limit = limit  # type: int
+
+        forcewin = self.flags & FORCEWIN
+        if isinstance(pats[0], bytes):
+            ptype = util.BYTES
+            self.current = b'.'  # type: AnyStr
+            self.specials = (b'.', b'..')  # type: Tuple[AnyStr, ...]
+            self.empty = b''  # type: AnyStr
+            self.stars = b'**'  # type: AnyStr
+            self.sep = b'\\' if forcewin else b'/'  # type: AnyStr
+            self.seps = (b'/', self.sep) if forcewin else (self.sep,)  # type: Tuple[AnyStr, ...]
+            self.re_pathlib_norm = cast(Pattern[AnyStr], _RE_WIN_PATHLIB_DOT_NORM[ptype])  # type: Pattern[AnyStr]
+            self.re_no_dir = cast(Pattern[AnyStr], _wcparse.RE_WIN_NO_DIR[ptype])  # type: Pattern[AnyStr]
         else:
-            self.sep = b'/' if self.is_bytes else '/'
-            self.seps = (self.sep,)
-            self.re_pathlib_norm = _RE_PATHLIB_DOT_NORM[util.BYTES if self.is_bytes else util.UNICODE]
-            self.re_no_dir = _wcparse.RE_NO_DIR[util.BYTES if self.is_bytes else util.UNICODE]
-        self._parse_patterns(pattern)
-
-        if (
-            (self.is_bytes and not isinstance(self.root_dir, bytes)) or
-            (not self.is_bytes and not isinstance(self.root_dir, str))
-        ):
+            ptype = util.UNICODE
+            self.current = '.'
+            self.specials = ('.', '..')
+            self.empty = ''
+            self.stars = '**'
+            self.sep = '\\' if forcewin else '/'
+            self.seps = ('/', self.sep) if forcewin else (self.sep,)
+            self.re_pathlib_norm = cast(Pattern[AnyStr], _RE_WIN_PATHLIB_DOT_NORM[ptype])
+            self.re_no_dir = cast(Pattern[AnyStr], _wcparse.RE_WIN_NO_DIR[ptype])
+
+        temp = os.fspath(root_dir) if root_dir is not None else self.current
+        if not isinstance(temp, type(pats[0])):
             raise TypeError(
                 'Pattern and root_dir should be of the same type, not {} and {}'.format(
-                    type(pattern[0]), type(self.root_dir)
+                    type(pats[0]), type(temp)
                 )
             )
 
-    def _iter_patterns(self, patterns):
+        self.root_dir = temp  # type: AnyStr
+        self._parse_patterns(pats)
+
+    def _iter_patterns(self, patterns: Sequence[AnyStr]) -> Iterator[Tuple[bool, AnyStr]]:
         """Iterate expanded patterns."""
 
         seen = set()
@@ -482,22 +501,20 @@ def _iter_patterns(self, patterns):
                     current_limit -= count
                     if current_limit < 1:
                         current_limit = 1
-        except _wcparse.bracex.ExpansionLimitException:
+        except bracex.ExpansionLimitException:
             raise _wcparse.PatternLimitException(
                 "Pattern limit exceeded the limit of {:d}".format(self.limit)
             )
 
-    def _parse_patterns(self, patterns):
+    def _parse_patterns(self, patterns: Sequence[AnyStr]) -> None:
         """Parse patterns."""
 
-        self.pattern = []
-        self.npatterns = []
         for is_neg, p in self._iter_patterns(patterns):
             if is_neg:
                 # Treat the inverse pattern as a normal pattern if it matches, we will exclude.
                 # This is faster as compiled patterns usually compare the include patterns first,
                 # and then the exclude, but glob will already know it wants to include the file.
-                self.npatterns.append(_wcparse._compile(p, self.negate_flags))
+                self.npatterns.append(cast(Pattern[AnyStr], _wcparse._compile(p, self.negate_flags)))
             else:
                 self.pattern.append(_GlobSplit(p, self.flags).split())
 
@@ -519,22 +536,22 @@ def _parse_patterns(self, patterns):
         ):
             self.nounique = True
 
-    def _is_hidden(self, name):
+    def _is_hidden(self, name: AnyStr) -> bool:
         """Check if is file hidden."""
 
         return not self.dot and name[0:1] == self.specials[0]
 
-    def _is_this(self, name):
+    def _is_this(self, name: AnyStr) -> bool:
         """Check if "this" directory `.`."""
 
         return name == self.specials[0] or name == self.sep
 
-    def _is_parent(self, name):
+    def _is_parent(self, name: AnyStr) -> bool:
         """Check if `..`."""
 
         return name == self.specials[1]
 
-    def _match_excluded(self, filename, is_dir):
+    def _match_excluded(self, filename: AnyStr, is_dir: bool) -> bool:
         """Check if file should be excluded."""
 
         if is_dir and not filename.endswith(self.sep):
@@ -548,21 +565,21 @@ def _match_excluded(self, filename, is_dir):
 
         return matched
 
-    def _is_excluded(self, path, is_dir):
+    def _is_excluded(self, path: AnyStr, is_dir: bool) -> bool:
         """Check if file is excluded."""
 
-        return self.npatterns and self._match_excluded(path, is_dir)
+        return bool(self.npatterns and self._match_excluded(path, is_dir))
 
-    def _match_literal(self, a, b=None):
+    def _match_literal(self, a: AnyStr, b: Optional[AnyStr] = None) -> bool:
         """Match two names."""
 
         return a.lower() == b if not self.case_sensitive else a == b
 
-    def _get_matcher(self, target):
+    def _get_matcher(self, target: Optional[Union[AnyStr, Pattern[AnyStr]]]) -> Optional[Callable[..., Any]]:
         """Get deep match."""
 
         if target is None:
-            matcher = None
+            matcher = None  # type: Optional[Callable[..., Any]]
         elif isinstance(target, (str, bytes)):
             # Plain text match
             if not self.case_sensitive:
@@ -575,7 +592,7 @@ def _get_matcher(self, target):
             matcher = target.match
         return matcher
 
-    def _lexists(self, path):
+    def _lexists(self, path: AnyStr) -> bool:
         """Check if file exists."""
 
         if not self.dir_fd:
@@ -587,7 +604,7 @@ def _lexists(self, path):
         else:
             return True
 
-    def prepend_base(self, path):
+    def prepend_base(self, path: AnyStr) -> AnyStr:
         """Join path to base if pattern is not absolute."""
 
         if self.is_abs_pattern:
@@ -595,13 +612,13 @@ def prepend_base(self, path):
         else:
             return os.path.join(self.root_dir, path)
 
-    def _iter(self, curdir, dir_only, deep):
+    def _iter(self, curdir: Optional[AnyStr], dir_only: bool, deep: bool) -> Iterator[Tuple[AnyStr, bool, bool, bool]]:
         """Iterate the directory."""
 
         try:
-            fd = None
+            fd = None  # type: Optional[int]
             if self.is_abs_pattern and curdir:
-                scandir = curdir
+                scandir = curdir  # type: Union[AnyStr, int]
             elif self.dir_fd is not None:
                 fd = scandir = os.open(
                     os.path.join(self.root_dir, curdir) if curdir else self.root_dir,
@@ -616,10 +633,10 @@ def _iter(self, curdir, dir_only, deep):
                 yield special, True, True, False
 
             try:
-                with os.scandir(scandir) as scan:
+                with os.scandir(scandir) as scan:  # type: ignore[type-var]
                     for f in scan:
                         try:
-                            hidden = self._is_hidden(f.name)
+                            hidden = self._is_hidden(f.name)  # type: ignore[arg-type]
                             is_dir = f.is_dir()
                             if is_dir:
                                 is_link = f.is_symlink()
@@ -627,7 +644,7 @@ def _iter(self, curdir, dir_only, deep):
                                 # We don't care if a file is a link
                                 is_link = False
                             if (not dir_only or is_dir):
-                                yield f.name, is_dir, hidden, is_link
+                                yield f.name, is_dir, hidden, is_link  # type: ignore[misc]
                         except OSError:  # pragma: no cover
                             pass
             finally:
@@ -637,7 +654,13 @@ def _iter(self, curdir, dir_only, deep):
         except OSError:  # pragma: no cover
             pass
 
-    def _glob_dir(self, curdir, matcher, dir_only=False, deep=False):
+    def _glob_dir(
+        self,
+        curdir: AnyStr,
+        matcher: Optional[Callable[..., Any]],
+        dir_only: bool = False,
+        deep: bool = False
+    ) -> Iterator[Tuple[AnyStr, bool]]:
         """Recursive directory glob."""
 
         files = list(self._iter(curdir, dir_only, deep))
@@ -655,7 +678,7 @@ def _glob_dir(self, curdir, matcher, dir_only=False, deep=False):
             if deep and not hidden and is_dir and follow:
                 yield from self._glob_dir(path, matcher, dir_only, deep)
 
-    def _glob(self, curdir, this, rest):
+    def _glob(self, curdir: AnyStr, part: _GlobPart, rest: List[_GlobPart]) -> Iterator[Tuple[AnyStr, bool]]:
         """
         Handle glob flow.
 
@@ -669,10 +692,10 @@ def _glob(self, curdir, this, rest):
         - `globstar` `**`.
         """
 
-        is_magic = this.is_magic
-        dir_only = this.dir_only
-        target = this.pattern
-        is_globstar = this.is_globstar
+        is_magic = part.is_magic
+        dir_only = part.dir_only
+        target = part.pattern
+        is_globstar = part.is_globstar
 
         if is_magic and is_globstar:
             # Glob star directory `**`.
@@ -730,7 +753,7 @@ def _glob(self, curdir, this, rest):
                 else:
                     yield path, is_dir
 
-    def _get_starting_paths(self, curdir, dir_only):
+    def _get_starting_paths(self, curdir: AnyStr, dir_only: bool) -> List[Tuple[AnyStr, bool]]:
         """
         Get the starting location.
 
@@ -751,7 +774,7 @@ def _get_starting_paths(self, curdir, dir_only):
             results = [(curdir, True)]
         return results
 
-    def is_unique(self, path):
+    def is_unique(self, path: AnyStr) -> bool:
         """Test if path is unique."""
 
         if self.nounique:
@@ -763,20 +786,20 @@ def is_unique(self, path):
             unique = True
         return unique
 
-    def _pathlib_norm(self, path):
+    def _pathlib_norm(self, path: AnyStr) -> AnyStr:
         """Normalize path as `pathlib` does."""
 
         path = self.re_pathlib_norm.sub(self.empty, path)
         return path[:-1] if len(path) > 1 and path[-1:] in self.seps else path
 
-    def format_path(self, path, is_dir, dir_only):
+    def format_path(self, path: AnyStr, is_dir: bool, dir_only: bool) -> Iterator[AnyStr]:
         """Format path."""
 
         path = os.path.join(path, self.empty) if dir_only or (self.mark and is_dir) else path
         if self.is_unique(self._pathlib_norm(path) if self.pathlib else path):
             yield path
 
-    def glob(self):
+    def glob(self) -> Iterator[AnyStr]:
         """Starts off the glob iterator."""
 
         curdir = self.current
@@ -829,26 +852,56 @@ def glob(self):
                             yield from self.format_path(match, is_dir, dir_only)
 
 
-def iglob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=_wcparse.PATTERN_LIMIT):
+def iglob(
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
+    dir_fd: Optional[int] = None,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> Iterator[AnyStr]:
     """Glob."""
 
-    yield from Glob(util.to_tuple(patterns), flags, root_dir, dir_fd, limit).glob()
+    if not isinstance(patterns, (str, bytes)) and not patterns:
+        return
+
+    yield from Glob(patterns, flags, root_dir, dir_fd, limit).glob()
 
 
-def glob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=_wcparse.PATTERN_LIMIT):
+def glob(
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
+    dir_fd: Optional[int] = None,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> List[AnyStr]:
     """Glob."""
 
     return list(iglob(patterns, flags=flags, root_dir=root_dir, dir_fd=dir_fd, limit=limit))
 
 
-def translate(patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
+def translate(
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> Tuple[List[AnyStr], List[AnyStr]]:
     """Translate glob pattern."""
 
     flags = _flag_transform(flags)
-    return _wcparse.translate(patterns, flags, limit)
-
-
-def globmatch(filename, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=_wcparse.PATTERN_LIMIT):
+    return _wcparse.translate(_wcparse.to_str_sequence(patterns), flags, limit)
+
+
+def globmatch(
+    filename: Union[AnyStr, 'os.PathLike[AnyStr]'],
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
+    dir_fd: Optional[int] = None,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> bool:
     """
     Check if filename matches pattern.
 
@@ -856,45 +909,79 @@ def globmatch(filename, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=
     but if `case_sensitive` is set, respect that instead.
     """
 
-    if root_dir is not None:
-        root_dir = os.fspath(root_dir)
+    pats = _wcparse.to_str_sequence(patterns)
 
+    # Shortcut out if we have no patterns
+    if not pats:
+        return False
+
+    ptype = type(pats[0])
+    rdir = os.fspath(root_dir) if root_dir is not None else root_dir
     flags = _flag_transform(flags)
-    filename = os.fspath(filename)
-    return _wcparse.compile(patterns, flags, limit).match(filename, root_dir, dir_fd)
+    fname = os.fspath(filename)
+
+    # Ensure types are not mismatched
+    if (rdir is not None and not isinstance(rdir, ptype)):
+        raise TypeError(
+            "Pattern type of '{}' does not match the resolved type of '{}' of the root dir".format(ptype, type(rdir))
+        )
+
+    return bool(_wcparse.compile(pats, flags, limit).match(fname, rdir, dir_fd))
+
+
+def globfilter(
+    filenames: Iterable[Union[AnyStr, 'os.PathLike[AnyStr]']],
+    patterns: Union[str, bytes, Sequence[AnyStr]],
+    *,
+    flags: int = 0,
+    root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
+    dir_fd: Optional[int] = None,
+    limit: int = _wcparse.PATTERN_LIMIT
+) -> List[Union[AnyStr, 'os.PathLike[AnyStr]']]:
+    """Filter names using pattern."""
 
+    pats = _wcparse.to_str_sequence(patterns)
 
-def globfilter(filenames, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=_wcparse.PATTERN_LIMIT):
-    """Filter names using pattern."""
+    # Shortcut out if we have no patterns
+    if not pats:
+        return []
+
+    ptype = type(pats[0])
+    rdir = os.fspath(root_dir) if root_dir is not None else root_dir
 
-    if root_dir is not None:
-        root_dir = os.fspath(root_dir)
+    # Ensure types are not mismatched
+    if (rdir is not None and not isinstance(rdir, ptype)):
+        raise TypeError(
+            "Pattern type of '{}' does not match the resolved type of '{}' of the root dir".format(ptype, type(rdir))
+        )
 
-    matches = []
+    matches = []  # type: List[Union[AnyStr, 'os.PathLike[AnyStr]']]
     flags = _flag_transform(flags)
-    obj = _wcparse.compile(patterns, flags, limit)
+    obj = _wcparse.compile(pats, flags, limit)
 
     for filename in filenames:
         temp = os.fspath(filename)
-        if obj.match(temp, root_dir, dir_fd):
+        if obj.match(temp, rdir, dir_fd):
             matches.append(filename)
     return matches
 
 
 @util.deprecated("This function will be removed in 9.0.")
-def raw_escape(pattern, unix=None, raw_chars=True):
+def raw_escape(pattern: AnyStr, unix: Optional[bool] = None, raw_chars: bool = True) -> AnyStr:
     """Apply raw character transform before applying escape."""
 
-    return _wcparse.escape(util.norm_pattern(pattern, False, raw_chars, True), unix=unix, pathname=True, raw=True)
+    return _wcparse.escape(
+        util.norm_pattern(pattern, False, raw_chars, True), unix=unix, pathname=True, raw=True
+    )
 
 
-def escape(pattern, unix=None):
+def escape(pattern: AnyStr, unix: Optional[bool] = None) -> AnyStr:
     """Escape."""
 
     return _wcparse.escape(pattern, unix=unix)
 
 
-def is_magic(pattern, *, flags=0):
+def is_magic(pattern: AnyStr, *, flags: int = 0) -> bool:
     """Check if the pattern is likely to be magic."""
 
     flags = _flag_transform(flags)
diff --git a/wcmatch/pathlib.py b/wcmatch/pathlib.py
index 5275784..e6fde4b 100644
--- a/wcmatch/pathlib.py
+++ b/wcmatch/pathlib.py
@@ -4,6 +4,7 @@
 from . import glob
 from . import _wcparse
 from . import util
+from typing import Iterable, Any, Union, Sequence, cast
 
 __all__ = (
     "CASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH",
@@ -68,67 +69,19 @@
 )
 
 
-class Path(pathlib.Path):
-    """Special pathlike object (which accesses the filesystem) that uses our own glob methods."""
-
-    __slots__ = ()
-
-    def __new__(cls, *args, **kwargs):
-        """New."""
-
-        if cls is Path:
-            cls = WindowsPath if os.name == 'nt' else PosixPath
-        if util.PY310:
-            self = cls._from_parts(args)
-        else:
-            self = cls._from_parts(args, init=False)
-        if not self._flavour.is_supported:
-            raise NotImplementedError("Cannot instantiate {!r} on your system".format(cls.__name__))
-        if not util.PY310:
-            self._init()
-        return self
-
-    def glob(self, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
-        """
-        Search the file system.
-
-        `GLOBSTAR` is enabled by default in order match the default behavior of `pathlib`.
-
-        """
-
-        if self.is_dir():
-            scandotdir = flags & SCANDOTDIR
-            flags = self._translate_flags(flags | _NOABSOLUTE) | ((_PATHLIB | SCANDOTDIR) if scandotdir else _PATHLIB)
-            for filename in glob.iglob(patterns, flags=flags, root_dir=str(self), limit=limit):
-                yield self.joinpath(filename)
-
-    def rglob(self, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
-        """
-        Recursive glob.
-
-        This uses the same recursive logic that the default `pathlib` object uses.
-        Folders and files are essentially matched from right to left.
-
-        `GLOBSTAR` is enabled by default in order match the default behavior of `pathlib`.
-
-        """
-
-        yield from self.glob(patterns, flags=flags | _EXTMATCHBASE, limit=limit)
-
-
 class PurePath(pathlib.PurePath):
     """Special pure pathlike object that uses our own glob methods."""
 
     __slots__ = ()
 
-    def __new__(cls, *args):
+    def __new__(cls, *args: str) -> 'PurePath':
         """New."""
 
         if cls is PurePath:
             cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
-        return cls._from_parts(args)
+        return cast('PurePath', cls._from_parts(args))  # type: ignore[attr-defined]
 
-    def _translate_flags(self, flags):
+    def _translate_flags(self, flags: int) -> int:
         """Translate flags for the current `pathlib` object."""
 
         flags = (flags & FLAG_MASK) | _PATHNAME
@@ -144,7 +97,7 @@ def _translate_flags(self, flags):
             flags |= _FORCEUNIX
         return flags
 
-    def _translate_path(self):
+    def _translate_path(self) -> str:
         """Translate the object to a path string and ensure trailing slash for non-pure paths that are directories."""
 
         sep = ''
@@ -154,7 +107,13 @@ def _translate_path(self):
 
         return name + sep
 
-    def match(self, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
+    def match(
+        self,
+        patterns: Union[str, Sequence[str]],
+        *,
+        flags: int = 0,
+        limit: int = _wcparse.PATTERN_LIMIT
+    ) -> bool:
         """
         Match patterns using `globmatch`, but also using the same right to left logic that the default `pathlib` uses.
 
@@ -167,7 +126,13 @@ def match(self, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
 
         return self.globmatch(patterns, flags=flags | _RTL, limit=limit)
 
-    def globmatch(self, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
+    def globmatch(
+        self,
+        patterns: Union[str, Sequence[str]],
+        *,
+        flags: int = 0,
+        limit: int = _wcparse.PATTERN_LIMIT
+    ) -> bool:
         """
         Match patterns using `globmatch`, but without the right to left logic that the default `pathlib` uses.
 
@@ -183,17 +148,79 @@ def globmatch(self, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):
         )
 
 
+class Path(pathlib.Path):
+    """Special pathlike object (which accesses the filesystem) that uses our own glob methods."""
+
+    __slots__ = ()
+
+    def __new__(cls, *args: str, **kwargs: Any) -> 'Path':
+        """New."""
+
+        if cls is Path:
+            cls = WindowsPath if os.name == 'nt' else PosixPath
+        if util.PY310:
+            self = cls._from_parts(args)  # type: ignore[attr-defined]
+        else:
+            self = cls._from_parts(args, init=False)  # type: ignore[attr-defined]
+        if not self._flavour.is_supported:
+            raise NotImplementedError("Cannot instantiate {!r} on your system".format(cls.__name__))
+        if not util.PY310:
+            self._init()
+        return cast('Path', self)
+
+    def glob(  # type: ignore[override]
+        self,
+        patterns: Union[str, Sequence[str]],
+        *,
+        flags: int = 0,
+        limit: int = _wcparse.PATTERN_LIMIT
+    ) -> Iterable['Path']:
+        """
+        Search the file system.
+
+        `GLOBSTAR` is enabled by default in order match the default behavior of `pathlib`.
+
+        """
+
+        if self.is_dir():
+            scandotdir = flags & SCANDOTDIR
+            flags = self._translate_flags(  # type: ignore[attr-defined]
+                flags | _NOABSOLUTE
+            ) | ((_PATHLIB | SCANDOTDIR) if scandotdir else _PATHLIB)
+            for filename in glob.iglob(patterns, flags=flags, root_dir=str(self), limit=limit):
+                yield self.joinpath(filename)
+
+    def rglob(  # type: ignore[override]
+        self,
+        patterns: Union[str, Sequence[str]],
+        *,
+        flags: int = 0,
+        limit: int = _wcparse.PATTERN_LIMIT
+    ) -> Iterable['Path']:
+        """
+        Recursive glob.
+
+        This uses the same recursive logic that the default `pathlib` object uses.
+        Folders and files are essentially matched from right to left.
+
+        `GLOBSTAR` is enabled by default in order match the default behavior of `pathlib`.
+
+        """
+
+        yield from self.glob(patterns, flags=flags | _EXTMATCHBASE, limit=limit)
+
+
 class PurePosixPath(PurePath):
     """Pure Posix path."""
 
-    _flavour = pathlib._posix_flavour
+    _flavour = pathlib._posix_flavour  # type: ignore[attr-defined]
     __slots__ = ()
 
 
 class PureWindowsPath(PurePath):
     """Pure Windows path."""
 
-    _flavour = pathlib._windows_flavour
+    _flavour = pathlib._windows_flavour  # type: ignore[attr-defined]
     __slots__ = ()
 
 
diff --git a/wcmatch/posix.py b/wcmatch/posix.py
index b3b1053..a52fcb2 100644
--- a/wcmatch/posix.py
+++ b/wcmatch/posix.py
@@ -62,7 +62,7 @@
 }
 
 
-def get_posix_property(value, limit_ascii=False):
+def get_posix_property(value: str, limit_ascii: bool = False) -> str:
     """Retrieve the POSIX category."""
 
     try:
diff --git a/wcmatch/util.py b/wcmatch/util.py
index b4d8f1e..9d275d2 100644
--- a/wcmatch/util.py
+++ b/wcmatch/util.py
@@ -7,6 +7,7 @@
 import unicodedata
 from functools import wraps
 import warnings
+from typing import Any, Callable, Tuple, AnyStr, Match, Pattern, Optional, cast
 
 PY37 = (3, 7) <= sys.version_info
 PY310 = (3, 10) <= sys.version_info
@@ -64,25 +65,19 @@
     _PLATFORM = "linux"
 
 
-def platform():
+def platform() -> str:
     """Get platform."""
 
     return _PLATFORM
 
 
-def is_case_sensitive():
+def is_case_sensitive() -> bool:
     """Check if case sensitive."""
 
     return CASE_FS
 
 
-def to_tuple(values):
-    """Combine values."""
-
-    return (values,) if isinstance(values, (str, bytes)) else tuple(values)
-
-
-def norm_pattern(pattern, normalize, is_raw_chars, ignore_escape=False):
+def norm_pattern(pattern: AnyStr, normalize: Optional[bool], is_raw_chars: bool, ignore_escape: bool = False) -> AnyStr:
     r"""
     Normalize pattern.
 
@@ -92,59 +87,68 @@ def norm_pattern(pattern, normalize, is_raw_chars, ignore_escape=False):
     - If `normalize` is enabled, take care to convert \/ to \\\\.
     """
 
-    is_bytes = isinstance(pattern, bytes)
+    if isinstance(pattern, bytes):
+        is_bytes = True
+        slash = b'\\'
+        multi_slash = slash * 4
+        pat = RE_BNORM
+    else:
+        is_bytes = False
+        slash = '\\'
+        multi_slash = slash * 4
+        pat = RE_NORM
 
     if not normalize and not is_raw_chars and not ignore_escape:
         return pattern
 
-    def norm(m):
+    def norm(m: Match[AnyStr]) -> AnyStr:
         """Normalize the pattern."""
 
         if m.group(1):
             char = m.group(1)
-            if normalize:
-                char = br'\\\\' if is_bytes else r'\\\\' if len(char) > 1 else char
+            if normalize and len(char) > 1:
+                char = multi_slash
         elif m.group(2):
-            char = BACK_SLASH_TRANSLATION[m.group(2)] if is_raw_chars else m.group(2)
+            char = cast(AnyStr, BACK_SLASH_TRANSLATION[m.group(2)] if is_raw_chars else m.group(2))
         elif is_raw_chars and m.group(4):
-            char = bytes([int(m.group(4), 8) & 0xFF]) if is_bytes else chr(int(m.group(4), 8))
+            char = cast(AnyStr, bytes([int(m.group(4), 8) & 0xFF]) if is_bytes else chr(int(m.group(4), 8)))
         elif is_raw_chars and m.group(3):
-            char = bytes([int(m.group(3)[2:], 16)]) if is_bytes else chr(int(m.group(3)[2:], 16))
+            char = cast(AnyStr, bytes([int(m.group(3)[2:], 16)]) if is_bytes else chr(int(m.group(3)[2:], 16)))
         elif is_raw_chars and not is_bytes and m.group(5):
-            char = unicodedata.lookup(m.group(5)[3:-1])
+            char = unicodedata.lookup(m.group(5)[3:-1])  # type: ignore[assignment]
         elif not is_raw_chars or m.group(5 if is_bytes else 6):
             char = m.group(0)
             if ignore_escape:
-                char = (b'\\' if is_bytes else '\\') + char
+                char = slash + char
         else:
             value = m.group(6) if is_bytes else m.group(7)
             pos = m.start(6) if is_bytes else m.start(7)
-            raise SyntaxError("Could not convert character value {} at position {:d}".format(value, pos))
+            raise SyntaxError("Could not convert character value {!r} at position {:d}".format(value, pos))
         return char
 
-    return (RE_BNORM if is_bytes else RE_NORM).sub(norm, pattern)
+    return pat.sub(norm, pattern)
 
 
-class StringIter(object):
+class StringIter:
     """Preprocess replace tokens."""
 
-    def __init__(self, string):
+    def __init__(self, string: str) -> None:
         """Initialize."""
 
         self._string = string
         self._index = 0
 
-    def __iter__(self):
+    def __iter__(self) -> "StringIter":
         """Iterate."""
 
         return self
 
-    def __next__(self):
+    def __next__(self) -> str:
         """Python 3 iterator compatible next."""
 
         return self.iternext()
 
-    def match(self, pattern):
+    def match(self, pattern: Pattern[str]) -> Optional[Match[str]]:
         """Perform regex match at index."""
 
         m = pattern.match(self._string, self._index)
@@ -153,22 +157,22 @@ def match(self, pattern):
         return m
 
     @property
-    def index(self):
+    def index(self) -> int:
         """Get current index."""
 
         return self._index
 
-    def previous(self):  # pragma: no cover
+    def previous(self) -> str:  # pragma: no cover
         """Get previous char."""
 
         return self._string[self._index - 1]
 
-    def advance(self, count):  # pragma: no cover
+    def advance(self, count: int) -> None:  # pragma: no cover
         """Advanced the index."""
 
         self._index += count
 
-    def rewind(self, count):
+    def rewind(self, count: int) -> None:
         """Rewind index."""
 
         if count > self._index:  # pragma: no cover
@@ -176,7 +180,7 @@ def rewind(self, count):
 
         self._index -= count
 
-    def iternext(self):
+    def iternext(self) -> str:
         """Iterate through characters of the string."""
 
         try:
@@ -188,24 +192,24 @@ def iternext(self):
         return char
 
 
-class Immutable(object):
+class Immutable:
     """Immutable."""
 
-    __slots__ = tuple()
+    __slots__: Tuple[Any, ...] = tuple()
 
-    def __init__(self, **kwargs):
+    def __init__(self, **kwargs: Any) -> None:
         """Initialize."""
 
         for k, v in kwargs.items():
             super(Immutable, self).__setattr__(k, v)
 
-    def __setattr__(self, name, value):  # pragma: no cover
+    def __setattr__(self, name: str, value: Any) -> None:  # pragma: no cover
         """Prevent mutability."""
 
         raise AttributeError('Class is immutable!')
 
 
-def is_hidden(path):
+def is_hidden(path: AnyStr) -> bool:
     """Check if file is hidden."""
 
     hidden = False
@@ -213,19 +217,19 @@ def is_hidden(path):
     if f[:1] in ('.', b'.'):
         # Count dot file as hidden on all systems
         hidden = True
-    elif _PLATFORM == 'windows':
+    elif sys.platform == 'win32':
         # On Windows, look for `FILE_ATTRIBUTE_HIDDEN`
-        FILE_ATTRIBUTE_HIDDEN = 0x2
         results = os.lstat(path)
+        FILE_ATTRIBUTE_HIDDEN = 0x2
         hidden = bool(results.st_file_attributes & FILE_ATTRIBUTE_HIDDEN)
-    elif _PLATFORM == "osx":  # pragma: no cover
+    elif sys.platform == "darwin":  # pragma: no cover
         # On macOS, look for `UF_HIDDEN`
         results = os.lstat(path)
         hidden = bool(results.st_flags & stat.UF_HIDDEN)
     return hidden
 
 
-def deprecated(message, stacklevel=2):  # pragma: no cover
+def deprecated(message: str, stacklevel: int = 2) -> Callable[..., Any]:  # pragma: no cover
     """
     Raise a `DeprecationWarning` when wrapped function/method is called.
 
@@ -236,9 +240,9 @@ def some_method()"
             pass
     """
 
-    def _wrapper(func):
+    def _wrapper(func: Callable[..., Any]) -> Callable[..., Any]:
         @wraps(func)
-        def _deprecated_func(*args, **kwargs):
+        def _deprecated_func(*args: Any, **kwargs: Any) -> Any:
             warnings.warn(
                 f"'{func.__name__}' is deprecated. {message}",
                 category=DeprecationWarning,
@@ -249,7 +253,7 @@ def _deprecated_func(*args, **kwargs):
     return _wrapper
 
 
-def warn_deprecated(message, stacklevel=2):  # pragma: no cover
+def warn_deprecated(message: str, stacklevel: int = 2) -> None:  # pragma: no cover
     """Warn deprecated."""
 
     warnings.warn(
diff --git a/wcmatch/wcmatch.py b/wcmatch/wcmatch.py
index 5a53e9a..38fc7af 100644
--- a/wcmatch/wcmatch.py
+++ b/wcmatch/wcmatch.py
@@ -23,7 +23,10 @@
 import os
 import re
 from . import _wcparse
+from . import _wcmatch
 from . import util
+from typing import Optional, Any, Iterator, List, Generic, AnyStr
+
 
 __all__ = (
     "CASE", "IGNORECASE", "RAWCHARS", "FILEPATHNAME", "DIRPATHNAME", "PATHNAME",
@@ -78,104 +81,62 @@
 )
 
 
-class _Mixin:  # pragma: no cover
-    """
-    DO NOT USE: Provide temporary methods to allow temporary, backwards compatibility for Rummage.
-
-    This is only a temporary solution to transition Rummage to a new way of overriding `glob`
-    style patterns with regular expressions (which is a niche need and not publicly supported).
-    It is advised to stick to the public, documented API. Anything else you use, you use at your
-    own risk.
-
-    Wildcard Match was originally written inside Rummage and moved out at a later point to be its
-    own library. The regular expression override of file patterns was something that didn't make
-    much sense in Wildcard match as a standalone project. The old way Rummage used to override the
-    file/folder checks was hacked in and was messy as it was based on internal knowledge of how
-    `WcMatch` worked.
-
-    NEW WAY:
-    We've provided a cleaner easier way to do this moving forward, and if we ever made the knowledge
-    of this public, this is the way we'd do it. If either `file_check` or `folder_exclude_check` is
-    initialized prior to us compiling the checks, we will skip compilation. This would happen in
-    `on_init`. Un-compiled patterns are stored in `pattern_file` and `pattern_folder_exclude`.
-
-    OLD WAY:
-    The reason we need these functions below is because the old way looked to see if `file_pattern`
-    or `exclude_pattern` already had a compiled object, and then would replace them with compiled
-    objects using the pattern that was already contained within. Rummage avoided recompiling
-    objects that were already compiled, but this was messy.
-    """
-
-    @property
-    def file_pattern(self):
-        """DO NOT USE: only provided for Rummage backwards compatibility, and will be remove in the future."""
-
-        return _wcparse.WcRegexp(tuple()) if not self.pattern_file else self.pattern_file
-
-    @property
-    def exclude_pattern(self):
-        """DO NOT USE: only provided for Rummage backwards compatibility, and will be remove in the future."""
-
-        return _wcparse.WcRegexp(tuple()) if not self.pattern_folder_exclude else self.pattern_folder_exclude
-
-    @file_pattern.setter
-    def file_pattern(self, value):
-        """DO NOT USE: only provided for Rummage backwards compatibility, and will be remove in the future."""
-
-        self.file_check = value
-
-    @exclude_pattern.setter
-    def exclude_pattern(self, value):
-        """DO NOT USE: only provided for Rummage backwards compatibility, and will be remove in the future."""
-
-        self.folder_exclude_check = value
-
-
-class WcMatch(_Mixin):
+class WcMatch(Generic[AnyStr]):
     """Finds files by wildcard."""
 
-    def __init__(self, root_dir, file_pattern=None, exclude_pattern=None, flags=0, limit=_wcparse.PATHNAME, **kwargs):
+    def __init__(
+        self,
+        root_dir: AnyStr,
+        file_pattern: Optional[AnyStr] = None,
+        exclude_pattern: Optional[AnyStr] = None,
+        flags: int = 0,
+        limit: int = _wcparse.PATHNAME,
+        **kwargs: Any
+    ):
         """Initialize the directory walker object."""
 
         self.is_bytes = isinstance(root_dir, bytes)
+        self._directory = self._norm_slash(root_dir)  # type: AnyStr
         self._abort = False
         self._skipped = 0
         self._parse_flags(flags)
-        self._directory = self._norm_slash(root_dir)
-        self._sep = os.fsencode(os.sep) if self.is_bytes else os.sep
-        self._root_dir = self._add_sep(self._get_cwd(), True)
+        self._sep = os.fsencode(os.sep) if isinstance(root_dir, bytes) else os.sep  # type: AnyStr
+        self._root_dir = self._add_sep(self._get_cwd(), True)  # type: AnyStr
         self.limit = limit
-        self.pattern_file = file_pattern if file_pattern else self._directory[0:0]
-        self.pattern_folder_exclude = exclude_pattern if exclude_pattern else self._directory[0:0]
-        self.file_check = None
-        self.folder_exclude_check = None
+        empty = os.fsencode('') if isinstance(root_dir, bytes) else ''
+        self.pattern_file = file_pattern if file_pattern is not None else empty  # type: AnyStr
+        self.pattern_folder_exclude = exclude_pattern if exclude_pattern is not None else empty  # type: AnyStr
+        self.file_check = None  # type: Optional[_wcmatch.WcRegexp[AnyStr]]
+        self.folder_exclude_check = None  # type: Optional[_wcmatch.WcRegexp[AnyStr]]
         self.on_init(**kwargs)
         self._compile(self.pattern_file, self.pattern_folder_exclude)
 
-    def _norm_slash(self, name):
+    def _norm_slash(self, name: AnyStr) -> AnyStr:
         """Normalize path slashes."""
 
-        if self.is_bytes:
-            return name.replace(b'/', b"\\") if not util.is_case_sensitive() else name
+        if util.is_case_sensitive():
+            return name
+        elif isinstance(name, bytes):
+            return name.replace(b'/', b"\\")
         else:
-            return name.replace('/', "\\") if not util.is_case_sensitive() else name
+            return name.replace('/', "\\")
 
-    def _add_sep(self, path, check=False):
+    def _add_sep(self, path: AnyStr, check: bool = False) -> AnyStr:
         """Add separator."""
 
         return (path + self._sep) if not check or not path.endswith(self._sep) else path
 
-    def _get_cwd(self):
+    def _get_cwd(self) -> AnyStr:
         """Get current working directory."""
 
         if self._directory:
             return self._directory
-        elif self.is_bytes:
+        elif isinstance(self._directory, bytes):
             return bytes(os.curdir, 'ASCII')
         else:
             return os.curdir
 
-    def _parse_flags(self, flags):
+    def _parse_flags(self, flags: int) -> None:
         """Parse flags."""
 
         self.flags = flags & FLAG_MASK
@@ -190,7 +151,7 @@ def _parse_flags(self, flags):
             self.flags |= _FORCEWIN
         self.flags = self.flags & (_wcparse.FLAG_MASK ^ MATCHBASE)
 
-    def _compile_wildcard(self, pattern, pathname=False):
+    def _compile_wildcard(self, pattern: AnyStr, pathname: bool = False) -> Optional[_wcmatch.WcRegexp[AnyStr]]:
         """Compile or format the wildcard inclusion/exclusion pattern."""
 
         flags = self.flags
@@ -199,14 +160,14 @@ def _compile_wildcard(self, pattern, pathname=False):
             if self.matchbase:
                 flags |= MATCHBASE
 
-        return _wcparse.compile(pattern, flags, self.limit) if pattern else None
+        return _wcparse.compile([pattern], flags, self.limit) if pattern else None
 
-    def _compile(self, file_pattern, folder_exclude_pattern):
+    def _compile(self, file_pattern: AnyStr, folder_exclude_pattern: AnyStr) -> None:
         """Compile patterns."""
 
         if self.file_check is None:
             if not file_pattern:
-                self.file_check = _wcparse.WcRegexp(
+                self.file_check = _wcmatch.WcRegexp(
                     (re.compile(br'^.*$' if isinstance(file_pattern, bytes) else r'^.*$', re.DOTALL),)
                 )
             else:
@@ -214,11 +175,11 @@ def _compile(self, file_pattern, folder_exclude_pattern):
 
         if self.folder_exclude_check is None:
             if not folder_exclude_pattern:
-                self.folder_exclude_check = _wcparse.WcRegexp(tuple())
+                self.folder_exclude_check = _wcmatch.WcRegexp(tuple())
             else:
                 self.folder_exclude_check = self._compile_wildcard(folder_exclude_pattern, self.dir_pathname)
 
-    def _valid_file(self, base, name):
+    def _valid_file(self, base: AnyStr, name: AnyStr) -> bool:
         """Return whether a file can be searched."""
 
         valid = False
@@ -229,17 +190,17 @@ def _valid_file(self, base, name):
             valid = False
         return self.on_validate_file(base, name) if valid else valid
 
-    def compare_file(self, filename):
+    def compare_file(self, filename: AnyStr) -> bool:
         """Compare filename."""
 
-        return self.file_check.match(filename)
+        return self.file_check.match(filename)  # type: ignore[union-attr]
 
-    def on_validate_file(self, base, name):
+    def on_validate_file(self, base: AnyStr, name: AnyStr) -> bool:
         """Validate file override."""
 
         return True
 
-    def _valid_folder(self, base, name):
+    def _valid_folder(self, base: AnyStr, name: AnyStr) -> bool:
         """Return whether a folder can be searched."""
 
         valid = True
@@ -247,7 +208,7 @@ def _valid_folder(self, base, name):
         if (
             not self.recursive or
             (
-                len(self.folder_exclude_check) and
+                self.folder_exclude_check and
                 not self.compare_directory(fullpath[self._base_len:] if self.dir_pathname else name)
             )
         ):
@@ -256,58 +217,60 @@ def _valid_folder(self, base, name):
             valid = False
         return self.on_validate_directory(base, name) if valid else valid
 
-    def compare_directory(self, directory):
+    def compare_directory(self, directory: AnyStr) -> bool:
         """Compare folder."""
 
-        return not self.folder_exclude_check.match(self._add_sep(directory) if self.dir_pathname else directory)
+        return not self.folder_exclude_check.match(  # type: ignore[union-attr]
+            self._add_sep(directory) if self.dir_pathname else directory
+        )
 
-    def on_init(self, **kwargs):
+    def on_init(self, **kwargs: Any) -> None:
         """Handle custom initialization."""
 
-    def on_validate_directory(self, base, name):
+    def on_validate_directory(self, base: AnyStr, name: AnyStr) -> bool:
         """Validate folder override."""
 
         return True
 
-    def on_skip(self, base, name):
+    def on_skip(self, base: AnyStr, name: AnyStr) -> Any:
         """On skip."""
 
         return None
 
-    def on_error(self, base, name):
+    def on_error(self, base: AnyStr, name: AnyStr) -> Any:
         """On error."""
 
         return None
 
-    def on_match(self, base, name):
+    def on_match(self, base: AnyStr, name: AnyStr) -> Any:
         """On match."""
 
         return os.path.join(base, name)
 
-    def on_reset(self):
+    def on_reset(self) -> None:
         """On reset."""
 
-    def get_skipped(self):
+    def get_skipped(self) -> int:
         """Get number of skipped files."""
 
         return self._skipped
 
-    def kill(self):
+    def kill(self) -> None:
         """Abort process."""
 
         self._abort = True
 
-    def is_aborted(self):
+    def is_aborted(self) -> bool:
         """Check if process has been aborted."""
 
         return self._abort
 
-    def reset(self):
+    def reset(self) -> None:
         """Revive class from a killed state."""
 
         self._abort = False
 
-    def _walk(self):
+    def _walk(self) -> Iterator[Any]:
         """Start search for valid files."""
 
         self._base_len = len(self._root_dir)
@@ -353,12 +316,12 @@ def _walk(self):
                     if self.is_aborted():
                         break
 
-    def match(self):
+    def match(self) -> List[Any]:
         """Run the directory walker."""
 
         return list(self.imatch())
 
-    def imatch(self):
+    def imatch(self) -> Iterator[Any]:
         """Run the directory walker as iterator."""
 
         self.on_reset()


Test patch:
diff --git a/requirements/test.txt b/requirements/test.txt
index 755c43c..a60b63c 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,3 +1,4 @@
 pytest
 pytest-cov
 coverage
+mypy
diff --git a/tests/test_fnmatch.py b/tests/test_fnmatch.py
index e852156..44cd65c 100644
--- a/tests/test_fnmatch.py
+++ b/tests/test_fnmatch.py
@@ -708,3 +708,22 @@ def test_limit_translate(self):
 
         with self.assertRaises(_wcparse.PatternLimitException):
             fnmatch.translate('{1..11}', flags=fnmatch.BRACE, limit=10)
+
+
+class TestTypes(unittest.TestCase):
+    """Test basic sequences."""
+
+    def test_match_set(self):
+        """Test `set` matching."""
+
+        self.assertTrue(fnmatch.fnmatch('a', set(['a'])))
+
+    def test_match_tuple(self):
+        """Test `tuple` matching."""
+
+        self.assertTrue(fnmatch.fnmatch('a', tuple(['a'])))
+
+    def test_match_list(self):
+        """Test `list` matching."""
+
+        self.assertTrue(fnmatch.fnmatch('a', ['a']))
diff --git a/tests/test_glob.py b/tests/test_glob.py
index 004fc7f..73d40cc 100644
--- a/tests/test_glob.py
+++ b/tests/test_glob.py
@@ -23,6 +23,9 @@
 import unittest
 import warnings
 import getpass
+from collections.abc import MutableMapping
+
+PY310 = (3, 10) <= sys.version_info
 
 # Below is general helper stuff that Python uses in `unittests`.  As these
 # not meant for users, and could change without notice, include them
@@ -34,6 +37,87 @@
 TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid())
 
 
+class EnvironmentVarGuard(MutableMapping):
+    """
+    Class to help protect the environment variable properly.  Can be used as a context manager.
+
+    Directly ripped from Python support tools so that we can actually try and test `~user` expansion.
+
+    Taken from: https://github.com/python/cpython/blob/main/Lib/test/support/os_helper.py.
+    """
+
+    def __init__(self):
+        """Initialize."""
+
+        self._environ = os.environ
+        self._changed = {}
+
+    def __getitem__(self, envvar):
+        """Get item."""
+
+        return self._environ[envvar]
+
+    def __setitem__(self, envvar, value):
+        """Set item."""
+
+        # Remember the initial value on the first access
+        if envvar not in self._changed:
+            self._changed[envvar] = self._environ.get(envvar)
+        self._environ[envvar] = value
+
+    def __delitem__(self, envvar):
+        """Delete item."""
+
+        # Remember the initial value on the first access
+        if envvar not in self._changed:
+            self._changed[envvar] = self._environ.get(envvar)
+        if envvar in self._environ:
+            del self._environ[envvar]
+
+    def keys(self):
+        """Get keys."""
+        return self._environ.keys()
+
+    def __iter__(self):
+        """Iterate."""
+
+        return iter(self._environ)
+
+    def __len__(self):
+        """Get length."""
+        return len(self._environ)
+
+    def set(self, envvar, value):  # noqa: A003
+        """Set variable."""
+
+        self[envvar] = value
+
+    def unset(self, envvar):
+        """Unset variable."""
+
+        del self[envvar]
+
+    def copy(self):
+        """Copy environment."""
+
+        # We do what `os.environ.copy()` does.
+        return dict(self)
+
+    def __enter__(self):
+        """Enter."""
+        return self
+
+    def __exit__(self, *ignore_exc):
+        """Exit."""
+        for (k, v) in self._changed.items():
+            if v is None:
+                if k in self._environ:
+                    del self._environ[k]
+            else:
+                self._environ[k] = v
+        os.environ = self._environ
+
+
 @contextlib.contextmanager
 def change_cwd(path, quiet=False):
     """
@@ -1543,19 +1627,34 @@ def test_tilde_bytes(self):
     def test_tilde_user(self):
         """Test tilde user cases."""
 
-        # Accommodate non-Windows user behavior
-        user = None
-        if not sys.platform.startswith('win'):
-            try:
-                user = getpass.getuser()
-            except ModuleNotFoundError:
-                pass
+        if sys.platform.startswith('win') and PY310:
+            # In CI, and maybe on other systems, we cannot be sure we'll be able to get the user.
+            # So fake it by using our own user name with the current `USERPROFILE` path.
+            with EnvironmentVarGuard() as env:
+                userpath = os.environ.get('USERPROFILE')
+                env.clear()
+                user = 'test_user'
+                env['USERPROFILE'] = userpath
+                env['USERNAME'] = 'test_user'
+
+                files = os.listdir(userpath)
+                self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files))
+        else:
+            # Accommodate non-Windows user behavior
+            user = None
+            if not sys.platform.startswith('win'):
+                try:
+                    user = getpass.getuser()
+                except ModuleNotFoundError:
+                    pass
 
-        if user is None:
-            user = os.path.basename(os.path.expanduser('~'))
+            if user is None:
+                # Last ditch effort to get a user.
+                user = os.path.basename(os.path.expanduser('~'))
 
-        files = os.listdir(os.path.expanduser('~{}'.format(user)))
-        self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files))
+            userpath = os.path.expanduser('~{}'.format(user))
+            files = os.listdir(userpath)
+            self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files))
 
     def test_tilde_disabled(self):
         """Test when tilde is disabled."""
@@ -1629,3 +1728,8 @@ def test_cwd_root_dir_str_bytes(self):
 
         with self.assertRaises(TypeError):
             glob.glob('docs/*', root_dir=b'.')
+
+    def test_cwd_root_dir_empty(self):
+        """Test empty patterns with current working directory."""
+
+        self.assertEqual(glob.glob([], root_dir='.'), [])
diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py
index 869d361..d2d8d85 100644
--- a/tests/test_globmatch.py
+++ b/tests/test_globmatch.py
@@ -996,6 +996,12 @@ def test_unfinished_ext(self):
             self.assertTrue(glob.globmatch(x + '(a|B', x + '(a|B', flags=flags))
             self.assertFalse(glob.globmatch(x + '(a|B', 'B', flags=flags))
 
+    def test_empty_pattern_lists(self):
+        """Test empty pattern lists."""
+
+        self.assertFalse(glob.globmatch('test', []))
+        self.assertEqual(glob.globfilter(['test'], []), [])
+
     def test_windows_drives(self):
         """Test windows drives."""
 
diff --git a/tests/test_wcparse.py b/tests/test_wcparse.py
index a534613..44b7834 100644
--- a/tests/test_wcparse.py
+++ b/tests/test_wcparse.py
@@ -45,23 +45,23 @@ def test_preprocessor_sequence(self):
     def test_compile_expansion_okay(self):
         """Test expansion is okay."""
 
-        self.assertEqual(len(_wcparse.compile('{1..10}', _wcparse.BRACE)), 10)
+        self.assertEqual(len(_wcparse.compile(['{1..10}'], _wcparse.BRACE)), 10)
 
     def test_compile_unique_optimization_okay(self):
         """Test that redundant patterns are reduced in compile."""
 
-        self.assertEqual(len(_wcparse.compile('|'.join(['a'] * 10), _wcparse.SPLIT, 10)), 1)
+        self.assertEqual(len(_wcparse.compile(['|'.join(['a'] * 10)], _wcparse.SPLIT, 10)), 1)
 
     def test_translate_expansion_okay(self):
         """Test expansion is okay."""
 
-        p1, p2 = _wcparse.translate('{1..10}', _wcparse.BRACE, 10)
+        p1, p2 = _wcparse.translate(['{1..10}'], _wcparse.BRACE, 10)
         count = len(p1) + len(p2)
         self.assertEqual(count, 10)
 
     def test_translate_unique_optimization_okay(self):
         """Test that redundant patterns are reduced in translate."""
-        p1, p2 = _wcparse.translate('|'.join(['a'] * 10), _wcparse.SPLIT, 10)
+        p1, p2 = _wcparse.translate(['|'.join(['a'] * 10)], _wcparse.SPLIT, 10)
         count = len(p1) + len(p2)
         self.assertEqual(count, 1)
 
@@ -69,14 +69,14 @@ def test_expansion_limt(self):
         """Test expansion limit."""
 
         with self.assertRaises(_wcparse.PatternLimitException):
-            _wcparse.compile('{1..11}', _wcparse.BRACE, 10)
+            _wcparse.compile(['{1..11}'], _wcparse.BRACE, 10)
 
         with self.assertRaises(_wcparse.PatternLimitException):
-            _wcparse.compile('|'.join(['a'] * 11), _wcparse.SPLIT, 10)
+            _wcparse.compile(['|'.join(['a'] * 11)], _wcparse.SPLIT, 10)
 
         with self.assertRaises(_wcparse.PatternLimitException):
             _wcparse.compile(
-                '{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5)),
+                ['{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5))],
                 _wcparse.SPLIT | _wcparse.BRACE, 10
             )
 
@@ -90,14 +90,14 @@ def test_expansion_limt_translation(self):
         """Test expansion limit."""
 
         with self.assertRaises(_wcparse.PatternLimitException):
-            _wcparse.translate('{1..11}', _wcparse.BRACE, 10)
+            _wcparse.translate(['{1..11}'], _wcparse.BRACE, 10)
 
         with self.assertRaises(_wcparse.PatternLimitException):
-            _wcparse.translate('|'.join(['a'] * 11), _wcparse.SPLIT, 10)
+            _wcparse.translate(['|'.join(['a'] * 11)], _wcparse.SPLIT, 10)
 
         with self.assertRaises(_wcparse.PatternLimitException):
             _wcparse.translate(
-                '{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5)),
+                ['{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5))],
                 _wcparse.SPLIT | _wcparse.BRACE, 10
             )
 
@@ -110,12 +110,12 @@ def test_expansion_limt_translation(self):
     def test_expansion_no_limit_compile(self):
         """Test no expansion limit compile."""
 
-        self.assertEqual(len(_wcparse.compile('{1..11}', _wcparse.BRACE, -1)), 11)
+        self.assertEqual(len(_wcparse.compile(['{1..11}'], _wcparse.BRACE, -1)), 11)
 
     def test_expansion_no_limit_translate(self):
         """Test no expansion limit translate."""
 
-        p1, p2 = _wcparse.translate('{1..11}', _wcparse.BRACE, 0)
+        p1, p2 = _wcparse.translate(['{1..11}'], _wcparse.BRACE, 0)
         count = len(p1) + len(p2)
         self.assertEqual(count, 11)
 
@@ -158,3 +158,9 @@ def test_unc_pattern(self):
             _wcparse.RE_WIN_DRIVE[0].match('//?/GLOBAL/UNC/server/mount/temp').group(0),
             '//?/GLOBAL/UNC/server/mount/'
         )
+
+    def test_bad_root_dir(self):
+        """Test bad root directory."""
+
+        with self.assertRaises(TypeError):
+            _wcparse.compile(['string'], _wcparse.PATHNAME | _wcparse.REALPATH, 0).match('string', b'rdir', None)