Repository: joke2k/faker
Pull request #1867 (2023-05-26 18:18:10)

PoC: a .optional proxy that may or may not return a fake value
### What does this change

This adds a `.optional` property, that should make all the usual functions return either a fake value or `None`, with a customisable probability.

This was a quick and dirty lunchtime project which could use some care and attention (in particular I'm not sure about the `prob=n` parameter, and the tests are a bit limited). I thought I'd get some feedback before spending too much time cleaning it up.

### What was wrong

Currently it can be a bit fiddly to create fake optional fake data. There are a few projects out there that extend `faker` to provide this functionality using custom providers, but they're limited in what they can generate. This provides access to everything that would normally be available.



Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index ec08707a..7d7d09e5 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 18.9.0
+current_version = 18.12.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 761465b0..199e41b6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,9 +4,8 @@ on:
   push:
     branches:
       - master
-  pull_request:
-    branches:
-      - master
+  pull_request_target:
+    types: [opened, synchronize, reopened]
 
 permissions:
   contents: read
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c8acd47..84f00fbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
 ## Changelog
 
+### [v18.12.0 - 2023-07-07](https://github.com/joke2k/faker/compare/v18.11.2...v18.12.0)
+
+*  Add fake automotive `vin` number function. Thanks @cyanghsieh.
+
+### [v18.11.2 - 2023-06-27](https://github.com/joke2k/faker/compare/v18.11.1...v18.11.2)
+
+* Fix area codes for `fr_FR` mobile phone numbers. Thanks @QuentinFchx.
+
+### [v18.11.1 - 2023-06-20](https://github.com/joke2k/faker/compare/v18.11.0...v18.11.1)
+
+*  Update Ukraine Country Code in `phone_number`. Thanks @lexxai.
+
+### [v18.11.0 - 2023-06-20](https://github.com/joke2k/faker/compare/v18.10.1...v18.11.0)
+
+* Add digit-above-two type to numerify function. Thanks @dlwrnc.
+
+### [v18.10.1 - 2023-06-02](https://github.com/joke2k/faker/compare/v18.10.0...v18.10.1)
+
+* Fix handling leap year in `en_US` Passport provider. Thanks @mgorny.
+
+### [v18.10.0 - 2023-06-01](https://github.com/joke2k/faker/compare/v18.9.1...v18.10.0)
+
+* Add `passport` Provider and `en_US` Implementation. Thanks @llw2128.
+
+### [v18.9.1 - 2023-06-01](https://github.com/joke2k/faker/compare/v18.9.0...v18.9.1)
+
+* Fix `fr_FR` `postcode` length. Thanks @vmttn.
+
 ### [v18.9.0 - 2023-05-16](https://github.com/joke2k/faker/compare/v18.8.0...v18.9.0)
 
 * Add `xml` provider. Thanks @Elihaybe.
diff --git a/VERSION b/VERSION
index f7e67778..3e558c9b 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-18.9.0
+18.12.0
diff --git a/docs/communityproviders.rst b/docs/communityproviders.rst
index 4aa887ca..c87ee2cf 100644
--- a/docs/communityproviders.rst
+++ b/docs/communityproviders.rst
@@ -44,6 +44,9 @@ Here's a list of Providers written by the community:
 | Posts         | Fake posts in markdown    | `mdgen`_                         |
 |               | format                    |                                  |
 +---------------+---------------------------+----------------------------------+
+| PySpark       | Fake PySpark DataFrane    | `faker_pyspark`_                 |
+|               | and Schema generator      |                                  |
++---------------+---------------------------+----------------------------------+
 | Vehicle       | Fake vehicle information  | `faker_vehicle`_                 |
 |               | includes Year Make Model  |                                  |
 +---------------+---------------------------+----------------------------------+
@@ -87,6 +90,7 @@ In order to be included, your provider must satisfy these requirement:
 .. _faker_microservice: https://pypi.org/project/faker-microservice/
 .. _faker_music: https://pypi.org/project/faker_music/
 .. _mdgen: https://pypi.org/project/mdgen/
+.. _faker_pyspark: https://pypi.org/project/faker-pyspark/
 .. _faker_vehicle: https://pypi.org/project/faker-vehicle/
 .. _faker_web: https://pypi.org/project/faker_web/
 .. _faker_wifi_essid: https://pypi.org/project/faker-wifi-essid/
diff --git a/docs/conf.py b/docs/conf.py
index bdebde7f..b0e8a42c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,9 +51,9 @@ copyright = "2014, Daniele Faraglia"
 # built documents.
 #
 # The short X.Y version.
-version = "18.9.0"
+version = "18.12.0"
 # The full version, including alpha/beta/rc tags.
-release = "18.9.0"
+release = "18.12.0"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/faker/__init__.py b/faker/__init__.py
index a5fd7c11..110cc3f6 100644
--- a/faker/__init__.py
+++ b/faker/__init__.py
@@ -2,6 +2,6 @@ from faker.factory import Factory
 from faker.generator import Generator
 from faker.proxy import Faker
 
-VERSION = "18.9.0"
+VERSION = "18.12.0"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/__init__.py b/faker/providers/__init__.py
index c329d512..e4932a4d 100644
--- a/faker/providers/__init__.py
+++ b/faker/providers/__init__.py
@@ -10,6 +10,7 @@ from ..utils.distribution import choices_distribution, choices_distribution_uniq
 
 _re_hash = re.compile(r"#")
 _re_perc = re.compile(r"%")
+_re_dol = re.compile(r"\$")
 _re_excl = re.compile(r"!")
 _re_at = re.compile(r"@")
 _re_qm = re.compile(r"\?")
@@ -325,6 +326,11 @@ class BaseProvider:
 
         return self.generator.random.randint(1, 9)
 
+    def random_digit_above_two(self) -> int:
+        """Generate a random digit above value two (2 to 9)."""
+
+        return self.generator.random.randint(2, 9)
+
     def random_digit_or_empty(self) -> Union[int, str]:
         """Generate a random digit (0 to 9) or an empty string.
 
@@ -603,6 +609,7 @@ class BaseProvider:
 
         - Number signs ('#') are replaced with a random digit (0 to 9).
         - Percent signs ('%') are replaced with a random non-zero digit (1 to 9).
+        - Dollar signs ('$') are replaced with a random digit above two (2 to 9).
         - Exclamation marks ('!') are replaced with a random digit or an empty string.
         - At symbols ('@') are replaced with a random non-zero digit or an empty string.
 
@@ -617,6 +624,7 @@ class BaseProvider:
         """
         text = _re_hash.sub(lambda x: str(self.random_digit()), text)
         text = _re_perc.sub(lambda x: str(self.random_digit_not_null()), text)
+        text = _re_dol.sub(lambda x: str(self.random_digit_above_two()), text)
         text = _re_excl.sub(lambda x: str(self.random_digit_or_empty()), text)
         text = _re_at.sub(lambda x: str(self.random_digit_not_null_or_empty()), text)
         return text
diff --git a/faker/providers/address/fr_FR/__init__.py b/faker/providers/address/fr_FR/__init__.py
index 1125716e..2ee23305 100644
--- a/faker/providers/address/fr_FR/__init__.py
+++ b/faker/providers/address/fr_FR/__init__.py
@@ -469,10 +469,10 @@ class Provider(AddressProvider):
 
     def postcode(self) -> str:
         """
-        Randomly returns a postcode generated from existing french depertment number.
+        Randomly returns a postcode generated from existing french department number.
         exemple: '33260'
         """
         department = self.department_number()
         if department in ["2A", "2B"]:
             department = "20"
-        return f"{department}{self.random_number(digits=5 - len(department))}"
+        return f"{department}{self.random_number(digits=5 - len(department), fix_len=True)}"
diff --git a/faker/providers/automotive/__init__.py b/faker/providers/automotive/__init__.py
index 4a3156e3..1f89d37c 100644
--- a/faker/providers/automotive/__init__.py
+++ b/faker/providers/automotive/__init__.py
@@ -7,6 +7,36 @@ from .. import BaseProvider, ElementsType
 localized = True
 
 
+def calculate_vin_str_weight(s: str, weight_factor: list) -> int:
+    """
+    multiply s(str) by weight_factor char by char
+    e.g.
+    input: s="ABCDE", weight_factor=[1, 2, 3, 4, 5]
+    return: A*1 + B*2 + C*3 + D*4 + E*5
+
+    will multiply 0 when len(weight_factor) less than len(s)
+    """
+
+    def _get_char_weight(c: str) -> int:
+        """A=1, B=2, ...., I=9,
+        J=1, K=2, ..., R=9,
+        S=2, T=3, ..., Z=9
+        """
+        if ord(c) <= 64:  # 0-9
+            return int(c)
+        if ord(c) <= 73:  # A-I
+            return ord(c) - 64
+        if ord(c) <= 82:  # J-R
+            return ord(c) - 73
+        # S-Z
+        return ord(c) - 81
+
+    res = 0
+    for i, c in enumerate(s):
+        res += _get_char_weight(c) * weight_factor[i] if i < len(weight_factor) else 0
+    return res
+
+
 class Provider(BaseProvider):
     """Implement default automotive provider for Faker."""
 
@@ -20,3 +50,14 @@ class Provider(BaseProvider):
             self.random_element(self.license_formats),
         )
         return self.numerify(temp)
+
+    def vin(self) -> str:
+        """Generate vin number."""
+        vin_chars = "1234567890ABCDEFGHJKLMNPRSTUVWXYZ"  # I, O, Q are restricted
+        front_part = self.bothify("????????", letters=vin_chars)
+        rear_part = self.bothify("????????", letters=vin_chars)
+        front_part_weight = calculate_vin_str_weight(front_part, [8, 7, 6, 5, 4, 3, 2, 10])
+        rear_part_weight = calculate_vin_str_weight(rear_part, [9, 8, 7, 6, 5, 4, 3, 2])
+        checksum = (front_part_weight + rear_part_weight) % 11
+        checksum_char = "X" if checksum == 10 else str(checksum)
+        return front_part + checksum_char + rear_part
diff --git a/faker/providers/automotive/en_PH/__init__.py b/faker/providers/automotive/en_PH/__init__.py
index f78c0f9f..71f05d3e 100644
--- a/faker/providers/automotive/en_PH/__init__.py
+++ b/faker/providers/automotive/en_PH/__init__.py
@@ -1,10 +1,10 @@
 from string import ascii_uppercase
 from typing import List
 
-from ... import BaseProvider
+from .. import Provider as AutomotiveProvider
 
 
-class Provider(BaseProvider):
+class Provider(AutomotiveProvider):
     """Implement automotive provider for ``en_PH`` locale.
 
     Vehicle registration in the Philippines has many controversies and is full
diff --git a/faker/providers/date_time/__init__.py b/faker/providers/date_time/__init__.py
index 329a2840..17b9debd 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -1809,6 +1809,7 @@ class Provider(BaseProvider):
         """
         Get a timestamp between January 1, 1970 and now, unless passed
         explicit start_datetime or end_datetime values.
+
         :example: 1061306726
         """
         start_datetime = self._parse_start_datetime(start_datetime)
@@ -1833,6 +1834,7 @@ class Provider(BaseProvider):
     ) -> datetime:
         """
         Get a datetime object for a date between January 1, 1970 and now
+
         :param tzinfo: timezone, instance of datetime.tzinfo subclass
         :example: datetime('2005-08-16 20:39:21')
         :return: datetime
@@ -1850,6 +1852,7 @@ class Provider(BaseProvider):
     ) -> datetime:
         """
         Get a datetime object for a date between January 1, 001 and now
+
         :param tzinfo: timezone, instance of datetime.tzinfo subclass
         :example: datetime('1265-03-22 21:15:52')
         :return: datetime
@@ -1883,6 +1886,7 @@ class Provider(BaseProvider):
     ) -> str:
         """
         Get a timestamp in ISO 8601 format (or one of its profiles).
+
         :param tzinfo: timezone, instance of datetime.tzinfo subclass
         :param sep: separator between date and time, defaults to 'T'
         :param timespec: format specifier for the time part, defaults to 'auto' - see datetime.isoformat() documentation
@@ -1903,6 +1907,7 @@ class Provider(BaseProvider):
     def date_object(self, end_datetime: Optional[datetime] = None) -> dtdate:
         """
         Get a date object between January 1, 1970 and now
+
         :example: datetime.date(2016, 9, 20)
         """
         return self.date_time(end_datetime=end_datetime).date()
@@ -1910,6 +1915,7 @@ class Provider(BaseProvider):
     def time(self, pattern: str = "%H:%M:%S", end_datetime: Optional[DateParseType] = None) -> str:
         """
         Get a time string (24h format by default)
+
         :param pattern: format
         :example: '15:02:34'
         """
@@ -1918,6 +1924,7 @@ class Provider(BaseProvider):
     def time_object(self, end_datetime: Optional[DateParseType] = None) -> dttime:
         """
         Get a time object
+
         :example: datetime.time(15, 56, 56, 772876)
         """
         return self.date_time(end_datetime=end_datetime).time()
diff --git a/faker/providers/passport/__init__.py b/faker/providers/passport/__init__.py
new file mode 100644
index 00000000..b624be1e
--- /dev/null
+++ b/faker/providers/passport/__init__.py
@@ -0,0 +1,45 @@
+import datetime
+import re
+
+from string import ascii_uppercase
+from typing import Tuple
+
+from .. import BaseProvider, ElementsType
+
+localized = True
+
+
+class Provider(BaseProvider):
+    """Implement default Passport provider for Faker."""
+
+    passport_number_formats: ElementsType = ()
+
+    def passport_dob(self) -> datetime.date:
+        """Generate a datetime date of bisrth."""
+        birthday = self.generator.date_of_birth()
+        return birthday
+
+    def passport_owner(self, gender: str = "X") -> Tuple[str, str]:
+        """Generate a given_name and surname for a passport owner
+        The ``gender`` argument is the gender marker of a passport owner, which is a one character string
+        that is either male, female, or non-binary.
+        """
+        if gender == "M":
+            given_name = self.generator.parse("{{first_name_male}}")
+        elif gender == "F":
+            given_name = self.generator.parse("{{first_name_female}}")
+        else:
+            given_name = self.generator.parse("{{first_name_nonbinary}}")
+
+        surname = self.generator.parse("{{last_name}}")
+
+        return given_name, surname
+
+    def passport_number(self) -> str:
+        """Generate a passport number by replacing tokens to be alphanumeric"""
+        temp = re.sub(
+            r"\?",
+            lambda x: self.random_element(ascii_uppercase),
+            self.random_element(self.passport_number_formats),
+        )
+        return self.numerify(temp)
diff --git a/faker/providers/passport/en_US/__init__.py b/faker/providers/passport/en_US/__init__.py
new file mode 100644
index 00000000..b0afb53c
--- /dev/null
+++ b/faker/providers/passport/en_US/__init__.py
@@ -0,0 +1,99 @@
+import random
+
+from datetime import date, timedelta
+from typing import Tuple
+
+from .. import Provider as PassportProvider
+
+
+class Provider(PassportProvider):
+    """Implement passport provider for ``en_US`` locale.
+
+    Sources:
+
+    - https://travel.state.gov/content/travel/en/passports/passport-help/next-generation-passport.html
+    - https://www.vitalrecordsonline.com/glossary/passport-book-number
+    """
+
+    passport_number_formats = (
+        # NGP
+        "?########",
+        # Pre-NGP
+        "#########",
+    )
+
+    def passport_dates(self, birthday: date = date.today()) -> Tuple[str, str, str]:
+        """Generates a formatted date of birth, issue, and expiration dates.
+        issue and expiration dates are conditioned to fall within U.S. standards of 5 and 10 year expirations
+
+
+        The ``birthday`` argument is a datetime.date object representing a date of birth.
+
+        Sources:
+
+        -https://travel.state.gov/content/travel/en/passports/passport-help/faqs.html
+        """
+        birth_date = birthday.strftime("%d ") + birthday.strftime("%b ") + birthday.strftime("%Y")
+        today = date.today()
+        age = (today - birthday).days // 365
+        if age < 16:
+            expiry_years = 5
+            issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
+            # Checks if age is less than 5 so issue date is not before birthdate
+            if age < 5:
+                issue_date = self.generator.date_time_between(birthday, today)
+        elif age >= 26:
+            expiry_years = 10
+            issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
+        else:
+            # In cases between age 16 and 26, the issue date is 5 years ago, but expiry may be in 10 or 5 years
+            expiry_years = 5
+            issue_date = self.generator.date_time_between(
+                today - timedelta(days=expiry_years * 365 - 1), birthday + timedelta(days=16 * 365 - 1)
+            )
+            # all people over 21 must have been over 16 when they recieved passport or it will be expired otherwise
+            if age >= 21:
+                issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
+                expiry_years = 10
+
+        if issue_date.day == 29 and issue_date.month == 2:
+            issue_date -= timedelta(days=1)
+        expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
+
+        issue_date_format = issue_date.strftime("%d ") + issue_date.strftime("%b ") + issue_date.strftime("%Y")
+        expiry_date_format = expiry_date.strftime("%d ") + expiry_date.strftime("%b ") + expiry_date.strftime("%Y")
+        return birth_date, issue_date_format, expiry_date_format
+
+    def passport_gender(self, seed: int = 0) -> str:
+        """Generates a string representing the gender displayed on a passport
+
+        Sources:
+
+        - https://williamsinstitute.law.ucla.edu/publications/x-gender-markers-passports/
+        """
+        if seed != 0:
+            random.seed(seed)
+
+        genders = ["M", "F", "X"]
+        gender = random.choices(genders, weights=[0.493, 0.493, 0.014], k=1)[0]
+        return gender
+
+    def passport_full(self) -> str:
+        """Generates a formatted sting with US Passport information"""
+        dob = self.passport_dob()
+        birth_date, issue_date, expiry_date = self.passport_dates(dob)
+        gender_g = self.passport_gender()
+        given_name, surname = self.passport_owner(gender=gender_g)
+        number = self.passport_number()
+
+        full_rep = """{first_name}\n{second_name}\n{gender}\n{dob}\n{issue}\n{expire}\n{num}\n"""
+        full_rep = full_rep.format(
+            first_name=given_name,
+            second_name=surname,
+            gender=gender_g,
+            dob=birth_date,
+            issue=issue_date,
+            expire=expiry_date,
+            num=number,
+        )
+        return full_rep
diff --git a/faker/providers/person/__init__.py b/faker/providers/person/__init__.py
index 272842bb..54760558 100644
--- a/faker/providers/person/__init__.py
+++ b/faker/providers/person/__init__.py
@@ -200,7 +200,7 @@ class Provider(BaseProvider):
 
     def name(self) -> str:
         """
-        :example 'John Doe'
+        :example: 'John Doe'
         """
         pattern: str = self.random_element(self.formats)
         return self.generator.parse(pattern)
diff --git a/faker/providers/phone_number/en_US/__init__.py b/faker/providers/phone_number/en_US/__init__.py
index ac1367e8..dc7dc7ee 100644
--- a/faker/providers/phone_number/en_US/__init__.py
+++ b/faker/providers/phone_number/en_US/__init__.py
@@ -4,45 +4,45 @@ from .. import Provider as PhoneNumberProvider
 class Provider(PhoneNumberProvider):
     formats = (
         # Standard 10-digit phone number formats
-        "##########",
-        "##########",
-        "###-###-####",
-        "###-###-####",
+        "$##$######",
+        "$##$######",
+        "$##-$##-####",
+        "$##-$##-####",
         # Optional 10-digit local phone number format
-        "(###)###-####",
-        "(###)###-####",
+        "($##)$##-####",
+        "($##)$##-####",
         # Non-standard 10-digit phone number format
-        "###.###.####",
-        "###.###.####",
+        "$##.$##.####",
+        "$##.$##.####",
         # Standard 10-digit phone number format with extensions
-        "###-###-####x###",
-        "###-###-####x####",
-        "###-###-####x#####",
+        "$##-$##-####x###",
+        "$##-$##-####x####",
+        "$##-$##-####x#####",
         # Optional 10-digit local phone number format with extensions
-        "(###)###-####x###",
-        "(###)###-####x####",
-        "(###)###-####x#####",
+        "($##)$##-####x###",
+        "($##)$##-####x####",
+        "($##)$##-####x#####",
         # Non-standard 10-digit phone number format with extensions
-        "###.###.####x###",
-        "###.###.####x####",
-        "###.###.####x#####",
+        "$##.$##.####x###",
+        "$##.$##.####x####",
+        "$##.$##.####x#####",
         # Standard 11-digit phone number format
-        "+1-###-###-####",
-        "001-###-###-####",
+        "+1-$##-$##-####",
+        "001-$##-$##-####",
         # Standard 11-digit phone number format with extensions
-        "+1-###-###-####x###",
-        "+1-###-###-####x####",
-        "+1-###-###-####x#####",
-        "001-###-###-####x###",
-        "001-###-###-####x####",
-        "001-###-###-####x#####",
+        "+1-$##-$##-####x###",
+        "+1-$##-$##-####x####",
+        "+1-$##-$##-####x#####",
+        "001-$##-$##-####x###",
+        "001-$##-$##-####x####",
+        "001-$##-$##-####x#####",
     )
 
     basic_formats = (
         # basic 10-digit phone number format with no extensions
-        "##########",
-        "###-###-####",
-        "(###)###-####",
+        "$##$######",
+        "$##-$##-####",
+        "($##)$##-####",
     )
 
     def basic_phone_number(self) -> str:
diff --git a/faker/providers/phone_number/fr_FR/__init__.py b/faker/providers/phone_number/fr_FR/__init__.py
index 1631b7c7..2b37951f 100644
--- a/faker/providers/phone_number/fr_FR/__init__.py
+++ b/faker/providers/phone_number/fr_FR/__init__.py
@@ -241,8 +241,34 @@ class Provider(PhoneNumberProvider):
         "594",
         "596",
         # mobile numbers
-        "6##",
-        "7##",
+        "60#",
+        "61#",
+        "62#",
+        "630",
+        "631",
+        "632",
+        "633",
+        "634",
+        "635",
+        "636",
+        "637",
+        "638",
+        "64#",
+        "65#",
+        "66#",
+        "67#",
+        "68#",
+        "695",
+        "698",
+        "699",
+        "73#",
+        "74#",
+        "75#",
+        "76#",
+        "77#",
+        "78#",
+        "79#",
+        # special numbers
         "80#",
     )
 
diff --git a/faker/providers/phone_number/uk_UA/__init__.py b/faker/providers/phone_number/uk_UA/__init__.py
index 1da25ee4..d26d3365 100644
--- a/faker/providers/phone_number/uk_UA/__init__.py
+++ b/faker/providers/phone_number/uk_UA/__init__.py
@@ -7,8 +7,8 @@ class Provider(PhoneNumberProvider):
         "### ## ##",
         "0## ### ## ##",
         "0## ###-##-##",
-        "+38 0## ###-##-##",
-        "+38 0## ###-##-##",
-        "+38 (0##) ###-##-##",
-        "+38 0## ### ## ##",
+        "+380 ## ###-##-##",
+        "+380 ## ###-##-##",
+        "+380 (##) ###-##-##",
+        "+380 ## ### ## ##",
     )
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 3cf61157..3118fb89 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -111,6 +111,11 @@ class Provider(BaseProvider):
     ) -> str:
         """
         Generates a random string of upper and lowercase letters.
+
+        :param min_chars: minimum length of the random part.
+        :param max_chars: maximum length of the random part.
+        :param prefix: an optional prefix to prepend to the random string.
+        :param suffix: an optional suffix to append to the random string.
         :return: Random of random length between min and max characters.
         """
         if min_chars is None:
diff --git a/faker/proxy.py b/faker/proxy.py
index 78f159bc..d2862eab 100644
--- a/faker/proxy.py
+++ b/faker/proxy.py
@@ -4,7 +4,7 @@ import re
 
 from collections import OrderedDict
 from random import Random
-from typing import Any, Callable, Dict, List, Optional, Pattern, Sequence, Tuple, Union
+from typing import Any, Callable, Dict, List, Optional, Pattern, Sequence, Tuple, TypeVar, Union
 
 from .config import DEFAULT_LOCALE
 from .exceptions import UniquenessException
@@ -15,6 +15,8 @@ from .utils.distribution import choices_distribution
 
 _UNIQUE_ATTEMPTS = 1000
 
+RetType = TypeVar("RetType")
+
 
 class Faker:
     """Proxy class capable of supporting multiple locales"""
@@ -36,6 +38,7 @@ class Faker:
         self._factory_map = OrderedDict()
         self._weights = None
         self._unique_proxy = UniqueProxy(self)
+        self._optional_proxy = OptionalProxy(self)
 
         if isinstance(locale, str):
             locales = [locale.replace("-", "_")]
@@ -137,6 +140,10 @@ class Faker:
     def unique(self) -> "UniqueProxy":
         return self._unique_proxy
 
+    @property
+    def optional(self) -> "OptionalProxy":
+        return self._optional_proxy
+
     def _select_factory(self, method_name: str) -> Factory:
         """
         Returns a random factory that supports the provider method
@@ -322,3 +329,37 @@ class UniqueProxy:
             return retval
 
         return wrapper
+
+
+class OptionalProxy:
+    """
+    Return either a fake value or None, with a customizable probability.
+    """
+    def __init__(self, proxy: Faker):
+        self._proxy = proxy
+
+    def __getattr__(self, name: str) -> Any:
+        obj = getattr(self._proxy, name)
+        if callable(obj):
+            return self._wrap(name, obj)
+        else:
+            raise TypeError("Accessing non-functions through .optional is not supported.")
+
+    def __getstate__(self):
+        # Copy the object's state from self.__dict__ which contains
+        # all our instance attributes. Always use the dict.copy()
+        # method to avoid modifying the original state.
+        state = self.__dict__.copy()
+        return state
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+
+    def _wrap(self, name: str, function: Callable[..., RetType]) -> Callable[..., Optional[RetType]]:
+        @functools.wraps(function)
+        def wrapper(*args: Any, prob: float = 0.5, **kwargs: Any) -> Optional[RetType]:
+            if not 0 < prob <= 1.0:
+                raise ValueError("prob must be between 0 and 1")
+            return function(*args, **kwargs) if self._proxy.boolean(chance_of_getting_true=int(prob * 100)) else None
+
+        return wrapper


Test patch:
diff --git a/tests/providers/__init__.py b/tests/providers/__init__.py
index 565a726c..efc9dec6 100644
--- a/tests/providers/__init__.py
+++ b/tests/providers/__init__.py
@@ -35,6 +35,10 @@ class TestBaseProvider:
         samples = [faker.random_digit_not_null() for _ in range(num_samples * 10)]
         assert set(samples) == set(range(1, 10))
 
+    def test_random_digit_above_two(self, faker, num_samples):
+        samples = [faker.random_digit_above_two() for _ in range(num_samples * 10)]
+        assert set(samples) == set(range(2, 10))
+
     def test_random_digit_or_empty(self, faker, num_samples):
         expected = set(range(10))
         expected.add("")
diff --git a/tests/providers/test_address.py b/tests/providers/test_address.py
index 0e5c47ef..35be9a81 100644
--- a/tests/providers/test_address.py
+++ b/tests/providers/test_address.py
@@ -870,8 +870,9 @@ class TestFrFr:
         for _ in range(num_samples):
             postcode = faker.postcode()
             assert isinstance(postcode, str)
+            assert len(postcode) == 5
             assert (
-                postcode[:3] in department_numbers  # for 3 digits deparments number
+                postcode[:3] in department_numbers  # for 3 digits departments number
                 or postcode[:2] == "20"  # for Corsica : "2A" or "2B"
                 or postcode[:2] in department_numbers  # any other
             )
diff --git a/tests/providers/test_automotive.py b/tests/providers/test_automotive.py
index 62f725b0..ad193bc1 100644
--- a/tests/providers/test_automotive.py
+++ b/tests/providers/test_automotive.py
@@ -2,6 +2,7 @@ import re
 
 from typing import Pattern
 
+from faker.providers.automotive import calculate_vin_str_weight
 from faker.providers.automotive.de_DE import Provider as DeDeAutomotiveProvider
 from faker.providers.automotive.es_ES import Provider as EsEsAutomotiveProvider
 from faker.providers.automotive.ro_RO import Provider as RoRoAutomotiveProvider
@@ -23,6 +24,19 @@ class _SimpleAutomotiveTestMixin:
             assert match is not None
             self.perform_extra_checks(license_plate, match)
 
+    def test_vin(self, faker, num_samples):
+        for _ in range(num_samples):
+            vin_number = faker.vin()
+            # length check: 17
+            assert len(vin_number) == 17
+
+            # verify checksum: vin_number[8]
+            front_part_weight = calculate_vin_str_weight(vin_number[:8], [8, 7, 6, 5, 4, 3, 2, 10])
+            rear_part_weight = calculate_vin_str_weight(vin_number[9:], [9, 8, 7, 6, 5, 4, 3, 2])
+            checksum = (front_part_weight + rear_part_weight) % 11
+            checksum_str = "X" if checksum == 10 else str(checksum)
+            assert vin_number[8] == checksum_str
+
 
 class TestArBh(_SimpleAutomotiveTestMixin):
     """Test ar_BH automotive provider methods"""
diff --git a/tests/test_optional.py b/tests/test_optional.py
new file mode 100644
index 00000000..f84bcdb3
--- /dev/null
+++ b/tests/test_optional.py
@@ -0,0 +1,44 @@
+import pytest
+
+from faker import Faker
+
+
+class TestOptionalClass:
+    def test_optional(self) -> None:
+        fake = Faker()
+
+        assert {fake.optional.boolean() for _ in range(10)} == {True, False, None}
+
+    def test_optional_probability(self) -> None:
+        """The probability is configurable."""
+        fake = Faker()
+
+        fake.optional.name(prob=0.1)
+
+    def test_optional_arguments(self) -> None:
+        """Other arguments are passed through to the function."""
+        fake = Faker()
+
+        fake.optional.pyint(1, 2, prob=0.4)
+
+    def test_optional_valid_range(self) -> None:
+        """Only probabilities in the range (0, 1]."""
+        fake = Faker()
+
+        with pytest.raises(ValueError, match=""):
+            fake.optional.name(prob=0)
+
+        with pytest.raises(ValueError, match=""):
+            fake.optional.name(prob=1.1)
+
+        with pytest.raises(ValueError, match=""):
+            fake.optional.name(prob=-3)
+
+    def test_functions_only(self):
+        """Accessing non-functions through the `.optional` attribute
+        will throw a TypeError."""
+
+        fake = Faker()
+
+        with pytest.raises(TypeError, match="Accessing non-functions through .optional is not supported."):
+            fake.optional.locales
diff --git a/tests/test_passport.py b/tests/test_passport.py
new file mode 100644
index 00000000..091b5ca0
--- /dev/null
+++ b/tests/test_passport.py
@@ -0,0 +1,82 @@
+from datetime import date, datetime, timedelta
+
+from faker.providers.person.en_US import Provider as EnUSPersonProvider
+
+female_first_name = list(EnUSPersonProvider.first_names_female.keys())
+male_first_name = list(EnUSPersonProvider.first_names_male.keys())
+nb_first_name = list(EnUSPersonProvider.first_names_nonbinary.keys())
+last_name = list(EnUSPersonProvider.last_names.keys())
+
+
+class TestPassportProvider:
+    def testDOB(self, faker, num_samples=100):
+        for _ in range(num_samples):
+            dob = faker.passport_dob()
+            age = (date.today() - dob).days // 365
+            assert isinstance(dob, date)
+            assert age <= 115
+            assert age >= 0
+
+    def testOwnerNames(self, faker, num_samples=100):
+        for _ in range(num_samples):
+            given_name, surname = faker.passport_owner("F")
+            assert given_name in female_first_name
+            assert surname in last_name
+
+            given_name, surname = faker.passport_owner("M")
+            assert given_name in male_first_name
+            assert surname in last_name
+
+            given_name, surname = faker.passport_owner("X")
+            assert given_name in nb_first_name
+            assert surname in last_name
+
+    def testNumFormat(self, faker, num_samples=100):
+        for _ in range(num_samples):
+            pass_number = faker.passport_number()
+            if pass_number[0].isalpha():
+                assert pass_number[1:].isdigit()
+            else:
+                assert pass_number.isdigit()
+            assert len(pass_number) == 9
+
+
+class TestEnUS:
+    def testDates(self, faker, num_samples=20):
+        age4 = date.today() - timedelta(days=4 * 365)
+        age12 = date.today() - timedelta(days=12 * 365)
+        age17 = date.today() - timedelta(days=17 * 365)
+        age23 = date.today() - timedelta(days=23 * 365)
+        age30 = date.today() - timedelta(days=30 * 365)
+
+        birthdays = [(age4, 4), (age12, 12), (age17, 17), (age23, 23), (age30, 30)]
+        for _ in range(num_samples):
+            for birthday in birthdays:
+                birth_date_f, issue_date_f, expiry_date_f = faker.passport_dates(birthday[0])
+                birth_date = datetime.strptime(birth_date_f, "%d %b %Y").date()
+                issue_date = datetime.strptime(issue_date_f, "%d %b %Y").date()
+                expiry_date = datetime.strptime(expiry_date_f, "%d %b %Y").date()
+
+                if birthday[1] < 21:
+                    assert (expiry_date - issue_date).days // 365 == 5
+                else:
+                    assert (expiry_date - issue_date).days // 365 == 10
+
+                assert expiry_date > issue_date
+                assert birth_date < issue_date
+
+    def testGender(self, faker, num_samples=100):
+        for _ in range(num_samples):
+            assert faker.passport_gender(7899) in ["M", "F", "X"]
+
+    def testfull(self, faker, num_samples=100):
+        for _ in range(num_samples):
+            pass_data = faker.passport_full().split("\n")
+            assert pass_data[1] in last_name
+            assert pass_data[2] in ["M", "F", "X"]
+            if pass_data[2] == "M":
+                assert pass_data[0] in male_first_name
+            elif pass_data[2] == "F":
+                assert pass_data[0] in female_first_name
+            elif pass_data[2] == "X":
+                assert pass_data[0] in nb_first_name
diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py
index c07b9081..f0f69dfa 100644
--- a/tests/utils/test_utils.py
+++ b/tests/utils/test_utils.py
@@ -87,6 +87,7 @@ class UtilsTestCase(unittest.TestCase):
                     "faker.providers.job",
                     "faker.providers.lorem",
                     "faker.providers.misc",
+                    "faker.providers.passport",
                     "faker.providers.person",
                     "faker.providers.phone_number",
                     "faker.providers.profile",