diff --git a/Pipfile.lock b/Pipfile.lock
index 46f6cb3..c22c04b 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -627,7 +627,7 @@
                 "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db",
                 "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"
             ],
-            "markers": "python_version < '3.5'",
+            "markers": "python_version < '3.6'",
             "version": "==2.3.5"
         },
         "pathspec": {
diff --git a/news/100.bugfix.rst b/news/100.bugfix.rst
new file mode 100644
index 0000000..176f4f1
--- /dev/null
+++ b/news/100.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed a bug which caused ``path_to_url`` to sometimes fail to properly encode surrogates using utf-8 on windows using python 3.
diff --git a/setup.cfg b/setup.cfg
index 8f0a877..c799b9d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,7 +41,7 @@ install_requires =
     backports.functools_lru_cache;python_version=="2.7"
     backports.shutil_get_terminal_size;python_version=="2.7"
     backports.weakref;python_version=="2.7"
-    pathlib2;python_version<"3.5"
+    pathlib2;python_version<"3.6"
     six
 
 [options.extras_require]
diff --git a/src/vistir/path.py b/src/vistir/path.py
index 2d6e80c..25d29eb 100644
--- a/src/vistir/path.py
+++ b/src/vistir/path.py
@@ -30,6 +30,14 @@ from .compat import (
     fs_encode,
 )
 
+# fmt: off
+if six.PY3:
+    from urllib.parse import quote_from_bytes as quote
+else:
+    from urllib import quote
+# fmt: on
+
+
 if IS_TYPE_CHECKING:
     from typing import Optional, Callable, Text, ByteString, AnyStr
 
@@ -158,13 +166,23 @@ def path_to_url(path):
     >>> path_to_url("/home/user/code/myrepo/myfile.zip")
     'file:///home/user/code/myrepo/myfile.zip'
     """
-    from .misc import to_text, to_bytes
+    from .misc import to_bytes
 
     if not path:
         return path
-    path = to_bytes(path, encoding="utf-8")
-    normalized_path = to_text(normalize_drive(os.path.abspath(path)), encoding="utf-8")
-    return to_text(Path(normalized_path).as_uri(), encoding="utf-8")
+    normalized_path = Path(normalize_drive(os.path.abspath(path))).as_posix()
+    if os.name == "nt" and normalized_path[1] == ":":
+        drive, _, path = normalized_path.partition(":")
+        # XXX: This enables us to handle half-surrogates that were never
+        # XXX: actually part of a surrogate pair, but were just incidentally
+        # XXX: passed in as a piece of a filename
+        quoted_path = quote(fs_encode(path))
+        return fs_decode("file:///{0}:{1}".format(drive, quoted_path))
+    # XXX: This is also here to help deal with incidental dangling surrogates
+    # XXX: on linux, by making sure they are preserved during encoding so that
+    # XXX: we can urlencode the backslash correctly
+    bytes_path = to_bytes(normalized_path, errors="backslashreplace")
+    return fs_decode("file://{0}".format(quote(bytes_path)))
 
 
 def url_to_path(url):
@@ -174,7 +192,6 @@ def url_to_path(url):
 
     Follows logic taken from pip's equivalent function
     """
-    from .misc import to_bytes
 
     assert is_file_url(url), "Only file: urls can be converted to local paths"
     _, netloc, path, _, _ = urllib_parse.urlsplit(url)
@@ -183,7 +200,7 @@ def url_to_path(url):
         netloc = "\\\\" + netloc
 
     path = urllib_request.url2pathname(netloc + path)
-    return to_bytes(path, encoding="utf-8")
+    return urllib_parse.unquote(path)
 
 
 def is_valid_url(url):

Python 3.6 Windows failure in test_path_to_url
https://dev.azure.com/sarugaku/vistir/_build/results?buildId=331&view=logs&jobId=89390fe7-33d2-53e5-d9b1-9b3b600e8bb2&j=89390fe7-33d2-53e5-d9b1-9b3b600e8bb2&t=b6f9f200-f93f-54fa-819b-ea7d35779bd6
```py
2019-12-12T01:01:03.3526250Z ______________________________ test_path_to_url _______________________________
2019-12-12T01:01:03.3527004Z 
2019-12-12T01:01:03.3527505Z     @given(fspaths())
2019-12-12T01:01:03.3528207Z >   @settings(suppress_health_check=(HealthCheck.filter_too_much,))
2019-12-12T01:01:03.3528670Z     def test_path_to_url(filepath):
2019-12-12T01:01:03.3529269Z 
2019-12-12T01:01:03.3529655Z tests\test_path.py:194: 
2019-12-12T01:01:03.3530134Z _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
2019-12-12T01:01:03.3530915Z tests\test_path.py:203: in test_path_to_url
2019-12-12T01:01:03.3531585Z     file_url = vistir.path.path_to_url(filename)
2019-12-12T01:01:03.3532281Z src\vistir\path.py:167: in path_to_url
2019-12-12T01:01:03.3533049Z     return to_text(Path(normalized_path).as_uri(), encoding="utf-8")
2019-12-12T01:01:03.3534223Z c:\hostedtoolcache\windows\python\3.6.8\x64\Lib\pathlib.py:722: in as_uri
2019-12-12T01:01:03.3534669Z     return self._flavour.make_uri(self)
2019-12-12T01:01:03.3536038Z _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
2019-12-12T01:01:03.3536432Z 
2019-12-12T01:01:03.3537360Z self = <pathlib._WindowsFlavour object at 0x00000175F035A0B8>
2019-12-12T01:01:03.3537862Z path = WindowsPath('D:/a/1/s/0\ud800')
2019-12-12T01:01:03.3538199Z 
2019-12-12T01:01:03.3538961Z     def make_uri(self, path):
2019-12-12T01:01:03.3541101Z         # Under Windows, file URIs use the UTF-8 encoding.
2019-12-12T01:01:03.3541798Z         drive = path.drive
2019-12-12T01:01:03.3542553Z         if len(drive) == 2 and drive[1] == ':':
2019-12-12T01:01:03.3542974Z             # It's a path on a local drive => 'file:///c:/a/b'
2019-12-12T01:01:03.3543468Z             rest = path.as_posix()[2:].lstrip('/')
2019-12-12T01:01:03.3543865Z             return 'file:///%s/%s' % (
2019-12-12T01:01:03.3544419Z >               drive, urlquote_from_bytes(rest.encode('utf-8')))
2019-12-12T01:01:03.3544907Z E           UnicodeEncodeError: 'utf-8' codec can't encode character '\ud800' in position 7: surrogates not allowed
2019-12-12T01:01:03.3545596Z 
2019-12-12T01:01:03.3546377Z c:\hostedtoolcache\windows\python\3.6.8\x64\Lib\pathlib.py:238: UnicodeEncodeError
2019-12-12T01:01:03.3547999Z --------------------------------- Hypothesis ----------------------------------
2019-12-12T01:01:03.3548531Z Falsifying example: test_path_to_url(filepath='0\ud800')
```
diff --git a/tests/test_path.py b/tests/test_path.py
index 590d1df..39aece1 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -196,6 +196,7 @@ def test_is_file_url_raises_valueerror_when_no_url_attribute_found():
 
 
 @given(fspaths())
+@example("0\ud800")
 @settings(suppress_health_check=(HealthCheck.filter_too_much,))
 def test_path_to_url(filepath):
     class FakeLink(object):

pip install -e .[dev,spinner,tests]
parse_log_pytest
Pipfile
pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
Arpeggio==2.0.2
attrs==25.3.0
babel==2.17.0
backports.tarfile==1.2.0
black==25.1.0
certifi==2025.1.31
cffi==1.17.1
cfgv==3.4.0
charset-normalizer==3.4.1
click==8.1.8
cmarkgfm==2024.11.20
colorama==0.4.6
coverage==4.5.4
cryptography==44.0.2
distlib==0.3.9
docutils==0.21.2
exceptiongroup==1.2.2
execnet==2.1.1
filelock==3.18.0
flake8==7.2.0
flake8-bugbear==24.12.12
hypothesis==6.130.5
hypothesis-fspaths==0.1
id==1.5.0
identify==2.6.9
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
invoke==2.2.0
isort==6.0.1
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
Jinja2==3.1.6
keyring==25.6.0
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mccabe==0.7.0
mdurl==0.1.2
more-itertools==10.6.0
mypy-extensions==1.0.0
nh3==0.2.21
nodeenv==1.9.1
packaging==24.2
parver==0.5
pathspec==0.12.1
pipfile==0.0.2
platformdirs==4.3.7
pluggy==1.5.0
pre_commit==4.2.0
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.1
Pygments==2.19.1
pytest==8.3.5
pytest-cov==2.10.1
pytest-timeout==2.3.1
pytest-xdist==3.6.1
pytoolconfig==1.3.1
PyYAML==6.0.2
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
rope==1.13.0
SecretStorage==3.3.3
six==1.17.0
snowballstemmer==2.2.0
sortedcontainers==2.4.0
Sphinx==7.4.7
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
termcolor==2.3.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==2.2.1
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
-e git+https://github.com/sarugaku/vistir.git@e11556fa09dec48f22105e5680dcd066f552ed23#egg=vistir
yaspin==3.1.0
zipp==3.21.0

name: vistir
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - pipfile=0.0.2=py_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - arpeggio==2.0.2
      - attrs==25.3.0
      - babel==2.17.0
      - backports-tarfile==1.2.0
      - black==25.1.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - cfgv==3.4.0
      - charset-normalizer==3.4.1
      - click==8.1.8
      - cmarkgfm==2024.11.20
      - colorama==0.4.6
      - coverage==4.5.4
      - cryptography==44.0.2
      - distlib==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - filelock==3.18.0
      - flake8==7.2.0
      - flake8-bugbear==24.12.12
      - hypothesis==6.130.5
      - hypothesis-fspaths==0.1
      - id==1.5.0
      - identify==2.6.9
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - invoke==2.2.0
      - isort==6.0.1
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==25.6.0
      - markdown-it-py==3.0.0
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - mdurl==0.1.2
      - more-itertools==10.6.0
      - mypy-extensions==1.0.0
      - nh3==0.2.21
      - nodeenv==1.9.1
      - packaging==24.2
      - parver==0.5
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pre-commit==4.2.0
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pytest==8.3.5
      - pytest-cov==2.10.1
      - pytest-timeout==2.3.1
      - pytest-xdist==3.6.1
      - pytoolconfig==1.3.1
      - pyyaml==6.0.2
      - readme-renderer==44.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - rope==1.13.0
      - secretstorage==3.3.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sortedcontainers==2.4.0
      - sphinx==7.4.7
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - termcolor==2.3.0
      - tomli==2.2.1
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - vistir==0.4.4.dev0
      - yaspin==3.1.0
      - zipp==3.21.0
prefix: /opt/conda/envs/vistir

tests/test_path.py::test_path_to_url
tests/test_path.py::test_abspathu tests/test_path.py::test_normalize_path tests/test_path.py::test_is_in_path[~/some/path/child-~/some/path-True] tests/test_path.py::test_is_in_path[~/some-~/some/path-False] tests/test_path.py::test_is_in_path[~/some/path/child-~-True] tests/test_path.py::test_safe_expandvars tests/test_path.py::test_mkdir_p tests/test_path.py::test_ensure_mkdir_p tests/test_path.py::test_create_tracked_tempdir tests/test_path.py::test_create_tracked_tempfile tests/test_path.py::test_rmtree tests/test_path.py::test_is_readonly_path tests/test_path.py::test_get_converted_relative_path tests/test_path.py::test_is_valid_url tests/test_path.py::test_none_as_valid_url_returns_none tests/test_path.py::test_is_file_url_returns_none_when_passed_none tests/test_path.py::test_normalize_drive tests/test_path.py::test_walk_up tests/test_path.py::test_handle_remove_readonly
Pipfile.lock setup.cfg news/100.bugfix.rst src/vistir/path.py
Pipfile.lock setup.cfg news/100.bugfix.rst src/vistir/path.py