codecov[bot]: # [Codecov](https://codecov.io/gh/pypa/twine/pull/322?src=pr&el=h1) Report
> Merging [#322](https://codecov.io/gh/pypa/twine/pull/322?src=pr&el=desc) into [master](https://codecov.io/gh/pypa/twine/commit/5199edbdce443ff8e0942424157820854c1cebf4?src=pr&el=desc) will **increase** coverage by `1.07%`.
> The diff coverage is `75%`.

[![Impacted file tree graph](https://codecov.io/gh/pypa/twine/pull/322/graphs/tree.svg?width=650&height=150&src=pr&token=NrvN4iFbj6)](https://codecov.io/gh/pypa/twine/pull/322?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #322      +/-   ##
==========================================
+ Coverage   68.51%   69.58%   +1.07%     
==========================================
  Files          12       12              
  Lines         578      582       +4     
  Branches       91       91              
==========================================
+ Hits          396      405       +9     
+ Misses        156      150       -6     
- Partials       26       27       +1
```


| [Impacted Files](https://codecov.io/gh/pypa/twine/pull/322?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [twine/utils.py](https://codecov.io/gh/pypa/twine/pull/322/diff?src=pr&el=tree#diff-dHdpbmUvdXRpbHMucHk=) | `87.12% <0%> (+1.98%)` | :arrow_up: |
| [twine/repository.py](https://codecov.io/gh/pypa/twine/pull/322/diff?src=pr&el=tree#diff-dHdpbmUvcmVwb3NpdG9yeS5weQ==) | `60.57% <100%> (+0.38%)` | :arrow_up: |
| [twine/exceptions.py](https://codecov.io/gh/pypa/twine/pull/322/diff?src=pr&el=tree#diff-dHdpbmUvZXhjZXB0aW9ucy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [twine/commands/upload.py](https://codecov.io/gh/pypa/twine/pull/322/diff?src=pr&el=tree#diff-dHdpbmUvY29tbWFuZHMvdXBsb2FkLnB5) | `65.47% <66.66%> (+3.57%)` | :arrow_up: |
| [twine/wininst.py](https://codecov.io/gh/pypa/twine/pull/322/diff?src=pr&el=tree#diff-dHdpbmUvd2luaW5zdC5weQ==) | `29.72% <0%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pypa/twine/pull/322?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pypa/twine/pull/322?src=pr&el=footer). Last update [5199edb...2697917](https://codecov.io/gh/pypa/twine/pull/322?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/docs/changelog.rst b/docs/changelog.rst
index 8c88876..4885afa 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,8 @@
 Changelog
 =========
 
+* :bug:`322 major` Raise exception if attempting upload to deprecated legacy
+  PyPI URLs.
 * :feature:`320` Remove PyPI as default ``register`` package index.
 * :feature:`319` Support Metadata 2.1 (:pep:`566`), including Markdown
   for ``description`` fields.
diff --git a/twine/commands/upload.py b/twine/commands/upload.py
index 9766893..e2ebdba 100644
--- a/twine/commands/upload.py
+++ b/twine/commands/upload.py
@@ -21,7 +21,7 @@ import sys
 
 import twine.exceptions as exc
 from twine.package import PackageFile
-from twine.repository import Repository, LEGACY_PYPI
+from twine.repository import Repository, LEGACY_PYPI, LEGACY_TEST_PYPI
 from twine import utils
 
 
@@ -98,11 +98,20 @@ def upload(dists, repository, sign, identity, username, password, comment,
 
     print("Uploading distributions to {0}".format(config["repository"]))
 
-    if config["repository"].startswith(LEGACY_PYPI):
-        print(
-            "Note: you are uploading to the old upload URL. It's recommended "
-            "to use the new URL \"{0}\" or to leave the URL unspecified and "
-            "allow twine to choose.".format(utils.DEFAULT_REPOSITORY))
+    if config["repository"].startswith((LEGACY_PYPI, LEGACY_TEST_PYPI)):
+        raise exc.UploadToDeprecatedPyPIDetected(
+            "You're trying to upload to the legacy PyPI site '{0}'. "
+            "Uploading to those sites is deprecated. \n "
+            "The new sites are pypi.org and test.pypi.org. Try using "
+            "{1} (or {2}) to upload your packages instead. "
+            "These are the default URLs for Twine now. \n More at "
+            "https://packaging.python.org/guides/migrating-to-pypi-org/ "
+            ".".format(
+                config["repository"],
+                utils.DEFAULT_REPOSITORY,
+                utils.TEST_REPOSITORY
+                )
+            )
 
     username = utils.get_username(username, config)
     password = utils.get_password(
diff --git a/twine/exceptions.py b/twine/exceptions.py
index 5783a9d..af0401c 100644
--- a/twine/exceptions.py
+++ b/twine/exceptions.py
@@ -30,3 +30,9 @@ class PackageNotFound(Exception):
     This is only used when attempting to register a package.
     """
     pass
+
+
+class UploadToDeprecatedPyPIDetected(Exception):
+    """An upload attempt was detected to deprecated legacy PyPI
+    sites pypi.python.org or testpypi.python.org."""
+    pass
diff --git a/twine/repository.py b/twine/repository.py
index 7d87d2d..74e1968 100644
--- a/twine/repository.py
+++ b/twine/repository.py
@@ -31,6 +31,7 @@ import twine
 KEYWORDS_TO_NOT_FLATTEN = set(["gpg_signature", "content"])
 
 LEGACY_PYPI = 'https://pypi.python.org/'
+LEGACY_TEST_PYPI = 'https://testpypi.python.org/'
 WAREHOUSE = 'https://upload.pypi.org/'
 OLD_WAREHOUSE = 'https://upload.pypi.io/'
 
diff --git a/twine/utils.py b/twine/utils.py
index 6f2ac30..5989de0 100644
--- a/twine/utils.py
+++ b/twine/utils.py
@@ -140,11 +140,15 @@ def normalize_repository_url(url):
 
 
 def check_status_code(response):
-    if (response.status_code == 500 and
+    """
+    Shouldn't happen, thanks to the UploadToDeprecatedPyPIDetected
+    exception, but this is in case that breaks and it does.
+    """
+    if (response.status_code == 410 and
             response.url.startswith(("https://pypi.python.org",
                                      "https://testpypi.python.org"))):
         print("It appears you're uploading to pypi.python.org (or "
-              "testpypi.python.org). You've received a 500 error response. "
+              "testpypi.python.org). You've received a 410 error response. "
               "Uploading to those sites is deprecated. The new sites are "
               "pypi.org and test.pypi.org. Try using "
               "https://upload.pypi.org/legacy/ "

notice obsolete repository settings and warn user more usefully
Following up on @tdicola's and others' comments in https://github.com/pypa/twine/issues/200#issuecomment-346537836 , #270, #258, here's a feature request:

If Twine notices that it's getting an obsolete `repository` setting (in particular `https://pypi.python.org/pypi`) from a `[repository]` flag in `.pypirc`, 

>  It would be really, really nice for twine to catch this case and give an actionable error since previous packaging instructions told people to add that repository line, etc.
diff --git a/tests/test_upload.py b/tests/test_upload.py
index 12ad867..55c20df 100644
--- a/tests/test_upload.py
+++ b/tests/test_upload.py
@@ -20,7 +20,7 @@ import pretend
 import pytest
 
 from twine.commands import upload
-from twine import package, cli
+from twine import package, cli, exceptions
 import twine
 
 import helpers
@@ -95,6 +95,40 @@ def test_get_config_old_format(tmpdir):
         ).format(pypirc)
 
 
+def test_deprecated_repo(tmpdir):
+    with pytest.raises(exceptions.UploadToDeprecatedPyPIDetected) as err:
+        pypirc = os.path.join(str(tmpdir), ".pypirc")
+        dists = ["tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"]
+
+        with open(pypirc, "w") as fp:
+            fp.write(textwrap.dedent("""
+                [pypi]
+                repository: https://pypi.python.org/pypi/
+                username:foo
+                password:bar
+            """))
+
+        upload.upload(dists=dists, repository="pypi", sign=None, identity=None,
+                      username=None, password=None, comment=None,
+                      cert=None, client_cert=None,
+                      sign_with=None, config_file=pypirc, skip_existing=False,
+                      repository_url=None,
+                      )
+
+        assert err.args[0] == (
+            "You're trying to upload to the legacy PyPI site "
+            "'https://pypi.python.org/pypi/'. "
+            "Uploading to those sites is deprecated. \n "
+            "The new sites are pypi.org and test.pypi.org. Try using "
+            "https://upload.pypi.org/legacy/ "
+            "(or https://test.pypi.org/legacy/) "
+            "to upload your packages instead. "
+            "These are the default URLs for Twine now. \n "
+            "More at "
+            "https://packaging.python.org/guides/migrating-to-pypi-org/ ."
+            )
+
+
 def test_skip_existing_skips_files_already_on_PyPI(monkeypatch):
     response = pretend.stub(
         status_code=400,

pip install -e .[dev]
parse_log_pytest
pytest
pytest coverage pretend
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
importlib-resources==5.4.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pkginfo==1.10.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
pretend==1.0.9
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
requests==2.27.1
requests-toolbelt==1.0.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tqdm==4.64.1
-e git+https://github.com/pypa/twine.git@5199edbdce443ff8e0942424157820854c1cebf4#egg=twine
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: twine
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
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==2.0.12
      - coverage==6.2
      - idna==3.10
      - importlib-resources==5.4.0
      - pkginfo==1.10.0
      - pretend==1.0.9
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - tqdm==4.64.1
      - urllib3==1.26.20
prefix: /opt/conda/envs/twine

tests/test_upload.py::test_deprecated_repo
tests/test_upload.py::test_ensure_wheel_files_uploaded_first tests/test_upload.py::test_ensure_if_no_wheel_files tests/test_upload.py::test_find_dists_expands_globs tests/test_upload.py::test_find_dists_errors_on_invalid_globs tests/test_upload.py::test_find_dists_handles_real_files tests/test_upload.py::test_get_config_old_format tests/test_upload.py::test_skip_existing_skips_files_already_on_PyPI tests/test_upload.py::test_skip_existing_skips_files_already_on_pypiserver tests/test_upload.py::test_skip_upload_respects_skip_existing tests/test_upload.py::test_values_from_env
twine/commands/upload.py twine/utils.py docs/changelog.rst twine/repository.py twine/exceptions.py
twine/commands/upload.py twine/utils.py docs/changelog.rst twine/repository.py twine/exceptions.py