diff --git a/twine/settings.py b/twine/settings.py
index 8e270fd..d4e3d1a 100644
--- a/twine/settings.py
+++ b/twine/settings.py
@@ -235,7 +235,11 @@ class Settings(object):
         )
 
     def _handle_authentication(self, username, password):
-        self.username = utils.get_username(username, self.repository_config)
+        self.username = utils.get_username(
+            self.repository_config['repository'],
+            username,
+            self.repository_config
+        )
         self.password = utils.get_password(
             self.repository_config['repository'],
             self.username,
diff --git a/twine/utils.py b/twine/utils.py
index 57eefb3..a4f6419 100644
--- a/twine/utils.py
+++ b/twine/utils.py
@@ -203,6 +203,23 @@ def get_userpass_value(cli_value, config, key, prompt_strategy=None):
         return None
 
 
+def get_username_from_keyring(system):
+    if 'keyring' not in sys.modules:
+        return
+
+    try:
+        getter = sys.modules['keyring'].get_credential
+    except AttributeError:
+        return None
+
+    try:
+        creds = getter(system, None)
+        if creds:
+            return creds.username
+    except Exception as exc:
+        warnings.warn(str(exc))
+
+
 def password_prompt(prompt_text):  # Always expects unicode for our own sanity
     prompt = prompt_text
     # Workaround for https://github.com/pypa/twine/issues/116
@@ -221,6 +238,13 @@ def get_password_from_keyring(system, username):
         warnings.warn(str(exc))
 
 
+def username_from_keyring_or_prompt(system):
+    return (
+        get_username_from_keyring(system)
+        or input_func('Enter your username: ')
+    )
+
+
 def password_from_keyring_or_prompt(system, username):
     return (
         get_password_from_keyring(system, username)
@@ -228,11 +252,18 @@ def password_from_keyring_or_prompt(system, username):
     )
 
 
-get_username = functools.partial(
-    get_userpass_value,
-    key='username',
-    prompt_strategy=functools.partial(input_func, 'Enter your username: '),
-)
+def get_username(system, cli_value, config):
+    return get_userpass_value(
+        cli_value,
+        config,
+        key='username',
+        prompt_strategy=functools.partial(
+            username_from_keyring_or_prompt,
+            system,
+        ),
+    )
+
+
 get_cacert = functools.partial(
     get_userpass_value,
     key='ca_cert',

Support keyring.get_username_and_password
The next release of keyring will include a [get_username_and_password](https://github.com/jaraco/keyring/issues/350) API that allows backends to return both a username and password for a URL.

By updating twine to use this API, we can enable backends for private feeds to use other mechanisms besides simple username/passwords to authenticate users.
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 520b78f..68d393d 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -229,6 +229,33 @@ def test_get_password_keyring_defers_to_prompt(monkeypatch):
     assert pw == 'entered pw'
 
 
+def test_get_username_and_password_keyring_overrides_prompt(monkeypatch):
+    import collections
+    Credential = collections.namedtuple('Credential', 'username password')
+
+    class MockKeyring:
+        @staticmethod
+        def get_credential(system, user):
+            return Credential(
+                'real_user',
+                'real_user@{system} sekure pa55word'.format(**locals())
+            )
+
+        @staticmethod
+        def get_password(system, user):
+            cred = MockKeyring.get_credential(system, user)
+            if user != cred.username:
+                raise RuntimeError("unexpected username")
+            return cred.password
+
+    monkeypatch.setitem(sys.modules, 'keyring', MockKeyring)
+
+    user = utils.get_username('system', None, {})
+    assert user == 'real_user'
+    pw = utils.get_password('system', user, None, {})
+    assert pw == 'real_user@system sekure pa55word'
+
+
 @pytest.fixture
 def keyring_missing(monkeypatch):
     """
@@ -237,11 +264,31 @@ def keyring_missing(monkeypatch):
     monkeypatch.delitem(sys.modules, 'keyring', raising=False)
 
 
+@pytest.fixture
+def keyring_missing_get_credentials(monkeypatch):
+    """
+    Simulate older versions of keyring that do not have the
+    'get_credentials' API.
+    """
+    monkeypatch.delattr('keyring.backends.KeyringBackend',
+                        'get_credential', raising=False)
+
+
+@pytest.fixture
+def entered_username(monkeypatch):
+    monkeypatch.setattr(utils, 'input_func', lambda prompt: 'entered user')
+
+
 @pytest.fixture
 def entered_password(monkeypatch):
     monkeypatch.setattr(utils, 'password_prompt', lambda prompt: 'entered pw')
 
 
+def test_get_username_keyring_missing_get_credentials_prompts(
+        entered_username, keyring_missing_get_credentials):
+    assert utils.get_username('system', None, {}) == 'entered user'
+
+
 def test_get_password_keyring_missing_prompts(
         entered_password, keyring_missing):
     assert utils.get_password('system', 'user', None, {}) == 'entered pw'
@@ -261,6 +308,28 @@ def keyring_no_backends(monkeypatch):
     monkeypatch.setitem(sys.modules, 'keyring', FailKeyring())
 
 
+@pytest.fixture
+def keyring_no_backends_get_credential(monkeypatch):
+    """
+    Simulate that keyring has no available backends. When keyring
+    has no backends for the system, the backend will be a
+    fail.Keyring, which raises RuntimeError on get_password.
+    """
+    class FailKeyring(object):
+        @staticmethod
+        def get_credential(system, username):
+            raise RuntimeError("fail!")
+    monkeypatch.setitem(sys.modules, 'keyring', FailKeyring())
+
+
+def test_get_username_runtime_error_suppressed(
+        entered_username, keyring_no_backends_get_credential, recwarn):
+    assert utils.get_username('system', None, {}) == 'entered user'
+    assert len(recwarn) == 1
+    warning = recwarn.pop(UserWarning)
+    assert 'fail!' in str(warning)
+
+
 def test_get_password_runtime_error_suppressed(
         entered_password, keyring_no_backends, recwarn):
     assert utils.get_password('system', 'user', None, {}) == 'entered pw'

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest coverage tox tox-pipenv
apt-get update apt-get install -y gcc
3.6
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
bleach==4.1.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
distlib==0.3.9
docutils==0.18.1
filelock==3.4.1
idna==3.10
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
packaging==21.3
pipenv==2022.4.8
pkginfo==1.10.0
platformdirs==2.4.0
pluggy==1.0.0
py==1.11.0
Pygments==2.14.0
pyparsing==3.1.4
pytest==7.0.1
readme-renderer==34.0
requests==2.27.1
requests-toolbelt==1.0.0
six==1.17.0
toml==0.10.2
tomli==1.2.3
tox==3.28.0
tox-pipenv==1.10.1
tqdm==4.64.1
-e git+https://github.com/pypa/twine.git@bd1d8b0f3ffdae9b91672d075d58cf635aa0e0f6#egg=twine
typing_extensions==4.1.1
urllib3==1.26.20
virtualenv==20.17.1
virtualenv-clone==0.5.7
webencodings==0.5.1
zipp==3.6.0

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
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - bleach==4.1.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - distlib==0.3.9
      - docutils==0.18.1
      - filelock==3.4.1
      - idna==3.10
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - packaging==21.3
      - pipenv==2022.4.8
      - pkginfo==1.10.0
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - py==1.11.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - readme-renderer==34.0
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - six==1.17.0
      - toml==0.10.2
      - tomli==1.2.3
      - tox==3.28.0
      - tox-pipenv==1.10.1
      - tqdm==4.64.1
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - virtualenv==20.17.1
      - virtualenv-clone==0.5.7
      - webencodings==0.5.1
      - zipp==3.6.0
prefix: /opt/conda/envs/twine

tests/test_utils.py::test_get_username_and_password_keyring_overrides_prompt tests/test_utils.py::test_get_username_keyring_missing_get_credentials_prompts tests/test_utils.py::test_get_username_runtime_error_suppressed
tests/test_utils.py::test_get_config tests/test_utils.py::test_get_config_no_distutils tests/test_utils.py::test_get_config_no_section tests/test_utils.py::test_get_config_override_pypi_url tests/test_utils.py::test_get_config_missing tests/test_utils.py::test_get_repository_config_missing tests/test_utils.py::test_get_config_deprecated_pypirc tests/test_utils.py::test_get_userpass_value[cli-config0-key-<lambda>-cli] tests/test_utils.py::test_get_userpass_value[None-config1-key-<lambda>-value] tests/test_utils.py::test_get_userpass_value[None-config2-key-<lambda>-fallback] tests/test_utils.py::test_default_to_environment_action[MY_PASSWORD-None-environ0-None] tests/test_utils.py::test_default_to_environment_action[MY_PASSWORD-None-environ1-foo] tests/test_utils.py::test_default_to_environment_action[URL-https://example.org-environ2-https://example.org] tests/test_utils.py::test_default_to_environment_action[URL-https://example.org-environ3-https://pypi.org] tests/test_utils.py::test_get_password_keyring_overrides_prompt tests/test_utils.py::test_get_password_keyring_defers_to_prompt tests/test_utils.py::test_get_password_keyring_missing_prompts tests/test_utils.py::test_get_password_runtime_error_suppressed tests/test_utils.py::test_no_positional_on_method tests/test_utils.py::test_no_positional_on_function
twine/utils.py twine/settings.py
twine/utils.py twine/settings.py