diff --git a/faker/providers/misc/__init__.py b/faker/providers/misc/__init__.py
index 32531a60..9b318691 100644
--- a/faker/providers/misc/__init__.py
+++ b/faker/providers/misc/__init__.py
@@ -88,13 +88,33 @@ class Provider(BaseProvider):
         @param lower_case: Boolean. Whether to use lower letters
         @return: String. Random password
         """
-        chars = ""
+        choices = ""
+        required_tokens = []
         if special_chars:
-            chars += "!@#$%^&*()_+"
+            required_tokens.append(random.choice("!@#$%^&*()_+"))
+            choices += "!@#$%^&*()_+"
         if digits:
-            chars += string.digits
+            required_tokens.append(random.choice(string.digits))
+            choices += string.digits
         if upper_case:
-            chars += string.ascii_uppercase
+            required_tokens.append(random.choice(string.ascii_uppercase))
+            choices += string.ascii_uppercase
         if lower_case:
-            chars += string.ascii_lowercase
-        return ''.join(random.choice(chars) for x in range(length))
+            required_tokens.append(random.choice(string.ascii_lowercase))
+            choices += string.ascii_lowercase
+
+        assert len(required_tokens) <= length, "Required length is shorter than required characters"
+
+        # Generate a first version of the password
+        chars = [random.choice(choices) for x in range(length)]
+
+        # Pick some unique locations
+        random_indexes = set()
+        while len(random_indexes) < len(required_tokens):
+            random_indexes.add(random.randint(0, len(chars) - 1))
+
+        # Replace them with the required characters
+        for i, index in enumerate(random_indexes):
+            chars[index] = required_tokens[i]
+
+        return ''.join(chars)

Param switches on faker.password() don't guarantee valid password
The format switches on `faker.password()` (`special_chars, digits, upper_case, lower_case`) don't always return passwords matching those rules.

This is problematic as when using generated passwords in unit tests, where passwords must conform to validity rules (e.g. "must contain numbers"), tests can randomly fail.

I expected that these switches would guarantee the function returns a conforming password. e.g. `faker.password(digits=True)` always returns a password containing digits, but this is not the case.

diff --git a/faker/tests/__init__.py b/faker/tests/__init__.py
index 4eeaa3c7..6502a448 100644
--- a/faker/tests/__init__.py
+++ b/faker/tests/__init__.py
@@ -9,6 +9,7 @@ import json
 import os
 import time
 import unittest
+import string
 import sys
 
 try:
@@ -458,6 +459,22 @@ class FactoryTestCase(unittest.TestCase):
             datetime.datetime.now(utc).replace(second=0, microsecond=0)
         )
 
+    def test_password(self):
+        from faker.providers.misc import Provider
+
+        def in_string(char, _str):
+            return char in _str
+
+        for _ in range(999):
+            password = Provider.password()
+
+            self.assertTrue(any([in_string(char, password) for char in "!@#$%^&*()_+"]))
+            self.assertTrue(any([in_string(char, password) for char in string.digits]))
+            self.assertTrue(any([in_string(char, password) for char in string.ascii_uppercase]))
+            self.assertTrue(any([in_string(char, password) for char in string.ascii_lowercase]))
+
+        self.assertRaises(AssertionError, Provider.password, length=2)
+
     def test_prefix_suffix_always_string(self):
         # Locales known to contain `*_male` and `*_female`.
         for locale in ("bg_BG", "dk_DK", "en", "ru_RU", "tr_TR"):

pip install -e .
parse_log_pytest
requirements.txt
pytest
3.9
faker/tests/requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
dnspython==2.7.0
email_validator==2.2.0
exceptiongroup==1.2.2
-e git+https://github.com/joke2k/faker.git@9f338881f582807fd9d1339b6148b039f8141bb3#egg=fake_factory
idna==3.10
iniconfig==2.1.0
mock==1.0.1
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
python-dateutil==2.9.0.post0
six==1.17.0
tomli==2.2.1
UkPostcodeParser==1.0.3

name: faker
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
  - 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
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - dnspython==2.7.0
      - email-validator==2.2.0
      - exceptiongroup==1.2.2
      - idna==3.10
      - iniconfig==2.1.0
      - mock==1.0.1
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - six==1.17.0
      - tomli==2.2.1
      - ukpostcodeparser==1.0.3
prefix: /opt/conda/envs/faker

faker/tests/__init__.py::FactoryTestCase::test_password
faker/tests/__init__.py::ShimsTestCase::test_counter faker/tests/__init__.py::UtilsTestCase::test_add_dicts faker/tests/__init__.py::UtilsTestCase::test_choice_distribution faker/tests/__init__.py::UtilsTestCase::test_find_available_locales faker/tests/__init__.py::UtilsTestCase::test_find_available_providers faker/tests/__init__.py::FactoryTestCase::test_add_provider_gives_priority_to_newly_added_provider faker/tests/__init__.py::FactoryTestCase::test_command faker/tests/__init__.py::FactoryTestCase::test_command_custom_provider faker/tests/__init__.py::FactoryTestCase::test_date_time_between_dates faker/tests/__init__.py::FactoryTestCase::test_date_time_between_dates_with_tzinfo faker/tests/__init__.py::FactoryTestCase::test_date_time_this_period faker/tests/__init__.py::FactoryTestCase::test_date_time_this_period_with_tzinfo faker/tests/__init__.py::FactoryTestCase::test_datetime_safe faker/tests/__init__.py::FactoryTestCase::test_datetimes_with_and_without_tzinfo faker/tests/__init__.py::FactoryTestCase::test_documentor faker/tests/__init__.py::FactoryTestCase::test_format_calls_formatter_on_provider faker/tests/__init__.py::FactoryTestCase::test_format_transfers_arguments_to_formatter faker/tests/__init__.py::FactoryTestCase::test_get_formatter_returns_callable faker/tests/__init__.py::FactoryTestCase::test_get_formatter_returns_correct_formatter faker/tests/__init__.py::FactoryTestCase::test_get_formatter_throws_exception_on_incorrect_formatter faker/tests/__init__.py::FactoryTestCase::test_magic_call_calls_format faker/tests/__init__.py::FactoryTestCase::test_magic_call_calls_format_with_arguments faker/tests/__init__.py::FactoryTestCase::test_no_words_paragraph faker/tests/__init__.py::FactoryTestCase::test_no_words_sentence faker/tests/__init__.py::FactoryTestCase::test_parse_returns_same_string_when_it_contains_no_curly_braces faker/tests/__init__.py::FactoryTestCase::test_parse_returns_string_with_tokens_replaced_by_formatters faker/tests/__init__.py::FactoryTestCase::test_prefix_suffix_always_string faker/tests/__init__.py::FactoryTestCase::test_random_element faker/tests/__init__.py::FactoryTestCase::test_slugify faker/tests/__init__.py::FactoryTestCase::test_timezone_conversion faker/tests/__init__.py::FactoryTestCase::test_us_ssn_valid faker/tests/__init__.py::GeneratorTestCase::test_random_seed_doesnt_seed_system_random
faker/providers/misc/__init__.py
faker/providers/misc/__init__.py