py-uk-postcode 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## 1.0.0 - 2026-08-13
6
+
7
+ ### Added
8
+
9
+ - Initial Python port of `postcode-js` 5.1.0.
10
+ - Typed postcode parsing and component extraction.
11
+ - Postcode shape validation, normalisation, matching, and replacement.
12
+ - Correction of common `O`/`0` and `I`/`1` input mistakes.
13
+ - JavaScript-compatible aliases to simplify migration.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Area360 (Python port)
4
+ Copyright (c) 2020 IDDQD Limited (Original implementation)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include CHANGELOG.md
2
+ include example.py
3
+ recursive-include assets *.png
4
+ recursive-include tests *.json *.py
@@ -0,0 +1,340 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-uk-postcode
3
+ Version: 1.0.0
4
+ Summary: UK postcode validation, parsing, formatting, and text utilities
5
+ Author: Area360
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/area360-uk/py-postcode
8
+ Project-URL: Documentation, https://github.com/area360-uk/py-postcode#readme
9
+ Project-URL: Repository, https://github.com/area360-uk/py-postcode
10
+ Project-URL: Issues, https://github.com/area360-uk/py-postcode/issues
11
+ Project-URL: Changelog, https://github.com/area360-uk/py-postcode/blob/main/CHANGELOG.md
12
+ Keywords: uk,postcode,validation,parsing
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.2; extra == "dev"
28
+ Requires-Dist: mypy>=1.11; extra == "dev"
29
+ Requires-Dist: pytest>=8; extra == "dev"
30
+ Requires-Dist: pytest-cov>=5; extra == "dev"
31
+ Requires-Dist: ruff>=0.6; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ <p align="center">
35
+ <img src="https://raw.githubusercontent.com/area360-uk/py-postcode/main/assets/py-postcode.png" alt="Py UK Postcode" width="100%">
36
+ </p>
37
+
38
+ # Py UK Postcode
39
+
40
+ > Validate and parse UK postcodes in Python
41
+
42
+ [![CI](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml/badge.svg)](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/py-uk-postcode.svg)](https://pypi.org/project/py-uk-postcode/)
44
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
45
+
46
+ Utility methods for UK postcodes, including validating the shape of a postcode
47
+ and extracting postcode elements such as incodes, outcodes, areas, and
48
+ [more](#definitions).
49
+
50
+ The implementation is a close Python port of [ideal-postcodes/postcode](https://github.com/ideal-postcodes/postcode), whose format rules
51
+ were tested against roughly 1.7 million postcodes from the ONS Postcode
52
+ Directory.
53
+
54
+ ## Features
55
+
56
+ - [Check](#validate) whether a postcode conforms to the
57
+ [correct format](https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#formatting)
58
+ - Small, single-purpose functions with no runtime dependencies
59
+ - [Extract](#parse) postcode elements such as the incode, outcode, and sector
60
+ - Search for and replace postcodes in larger bodies of text
61
+ - Correct common `O`/`0` and `I`/`1` input mistakes
62
+ - Fully typed Python API
63
+
64
+ ## Acknowledgments and credits
65
+
66
+ `py-uk-postcode` is a Python port of the original [ideal-postcodes/postcode](https://github.com/ideal-postcodes/postcode), created
67
+ by Ideal Postcodes and released under the MIT License. The original project's
68
+ API design, implementation, documentation, and test fixtures formed the basis
69
+ of this port.
70
+
71
+ ## Links
72
+
73
+ - [GitHub repository](https://github.com/area360-uk/py-postcode)
74
+ - [Package on PyPI](https://pypi.org/project/py-uk-postcode/)
75
+ - [Issue tracker](https://github.com/area360-uk/py-postcode/issues)
76
+ - [Postcode element definitions](#definitions)
77
+ - [Notes](#notes)
78
+
79
+ ## Getting started
80
+
81
+ ### Installation
82
+
83
+ With `pip`:
84
+
85
+ ```bash
86
+ python -m pip install py-uk-postcode
87
+ ```
88
+
89
+ With `uv`:
90
+
91
+ ```bash
92
+ uv add py-uk-postcode
93
+ ```
94
+
95
+ With Poetry:
96
+
97
+ ```bash
98
+ poetry add py-uk-postcode
99
+ ```
100
+
101
+ With PDM:
102
+
103
+ ```bash
104
+ pdm add py-uk-postcode
105
+ ```
106
+
107
+ With Conda, create or activate an environment and install the package from
108
+ PyPI:
109
+
110
+ ```bash
111
+ conda create --name postcode python=3.14 pip
112
+ conda activate postcode
113
+ python -m pip install py-uk-postcode
114
+ ```
115
+
116
+ The distribution is named `py-uk-postcode`; import it in Python as `postcode`.
117
+
118
+ ### Validate
119
+
120
+ ```python
121
+ from postcode import is_valid
122
+
123
+ is_valid("AA1 1AB") # => True
124
+ ```
125
+
126
+ ### Parse
127
+
128
+ Pass a string to `parse()`. It returns either a `ValidPostcode` or an
129
+ `InvalidPostcode`, both of which expose their values as attributes.
130
+
131
+ #### Valid postcode
132
+
133
+ ```python
134
+ from postcode import parse
135
+
136
+ result = parse("Sw1A 2aa")
137
+
138
+ result.postcode # => "SW1A 2AA"
139
+ result.outcode # => "SW1A"
140
+ result.incode # => "2AA"
141
+ result.area # => "SW"
142
+ result.district # => "SW1"
143
+ result.unit # => "AA"
144
+ result.sector # => "SW1A 2"
145
+ result.sub_district # => "SW1A"
146
+ result.valid # => True
147
+ ```
148
+
149
+ #### Invalid postcode
150
+
151
+ ```python
152
+ result = parse(" Oh no, ): ")
153
+
154
+ result.postcode # => None
155
+ result.outcode # => None
156
+ result.incode # => None
157
+ result.area # => None
158
+ result.district # => None
159
+ result.unit # => None
160
+ result.sector # => None
161
+ result.sub_district # => None
162
+ result.valid # => False
163
+ ```
164
+
165
+ #### Type narrowing
166
+
167
+ `parse()` returns a typed union. Type checkers can narrow it by checking the
168
+ literal `valid` attribute:
169
+
170
+ ```python
171
+ from postcode import parse
172
+
173
+ result = parse("SW1A 2AA")
174
+
175
+ if result.valid:
176
+ print(result.outcode.lower())
177
+ if result.sub_district is not None:
178
+ print(result.sub_district.lower())
179
+ else:
180
+ print("Invalid postcode")
181
+ ```
182
+
183
+ #### Valid postcode object
184
+
185
+ | Postcode | `.outcode` | `.incode` | `.area` | `.district` | `.sub_district` | `.sector` | `.unit` |
186
+ |----------|------------|-----------|---------|-------------|-----------------|-----------|---------|
187
+ | AA9A 9AA | AA9A | 9AA | AA | AA9 | AA9A | AA9A 9 | AA |
188
+ | A9A 9AA | A9A | 9AA | A | A9 | A9A | A9A 9 | AA |
189
+ | A9 9AA | A9 | 9AA | A | A9 | `None` | A9 9 | AA |
190
+ | A99 9AA | A99 | 9AA | A | A99 | `None` | A99 9 | AA |
191
+ | AA9 9AA | AA9 | 9AA | AA | AA9 | `None` | AA9 9 | AA |
192
+ | AA99 9AA | AA99 | 9AA | AA | AA99 | `None` | AA99 9 | AA |
193
+
194
+ ### Exported functions
195
+
196
+ If you need a single value, import the corresponding function directly.
197
+
198
+ #### Validation
199
+
200
+ ```python
201
+ from postcode import is_valid, valid_outcode
202
+
203
+ is_valid("Sw1A 2aa") # => True
204
+ valid_outcode("SW1A") # => True
205
+ ```
206
+
207
+ #### Formatting
208
+
209
+ ```python
210
+ from postcode import (
211
+ to_area,
212
+ to_district,
213
+ to_incode,
214
+ to_normalised,
215
+ to_outcode,
216
+ to_sector,
217
+ to_sub_district,
218
+ to_unit,
219
+ )
220
+
221
+ to_normalised("Sw1A 2aa") # => "SW1A 2AA"
222
+ to_outcode("Sw1A 2aa") # => "SW1A"
223
+ to_incode("Sw1A 2aa") # => "2AA"
224
+ to_area("Sw1A 2aa") # => "SW"
225
+ to_district("Sw1A 2aa") # => "SW1"
226
+ to_sub_district("Sw1A 2aa") # => "SW1A"
227
+ to_sector("Sw1A 2aa") # => "SW1A 2"
228
+ to_unit("Sw1A 2aa") # => "AA"
229
+ ```
230
+
231
+ All formatting functions return `None` when given an invalid postcode.
232
+
233
+ #### Fix
234
+
235
+ `fix()` attempts to clean up a postcode without validating it. It replaces
236
+ commonly confused characters (`O`/`0` and `I`/`1`), uppercases the value, and
237
+ corrects its spacing. If the input cannot be reliably fixed, the original
238
+ string is returned.
239
+
240
+ ```python
241
+ from postcode import fix, parse
242
+
243
+ fix("SWIA 2AA") # => "SW1A 2AA"
244
+ fix("SW1A 21A") # => "SW1A 2IA"
245
+ fix("SW1A OAA") # => "SW1A 0AA"
246
+ fix("SW1A 20A") # => "SW1A 2OA"
247
+ fix(" SW1A 2AO") # => "SW1A 2AO"
248
+ fix("sw1a 2aa") # => "SW1A 2AA"
249
+
250
+ result = parse(fix("SW1A 2A0"))
251
+ result.incode # => "2AO"
252
+
253
+ fix("12a") # => "12a"
254
+ ```
255
+
256
+ #### Extract and replace
257
+
258
+ `match()` retrieves postcode-shaped values from a body of text. Matches retain
259
+ their original casing and spacing.
260
+
261
+ ```python
262
+ from postcode import match, to_normalised, to_outcode
263
+
264
+ matches = match("The two addresses are SW1A2aa and SW1A 2AB")
265
+ # => ["SW1A2aa", "SW1A 2AB"]
266
+
267
+ [to_normalised(value) for value in matches]
268
+ # => ["SW1A 2AA", "SW1A 2AB"]
269
+
270
+ [to_outcode(value) for value in matches]
271
+ # => ["SW1A", "SW1A"]
272
+
273
+ match("Some London outward codes are SW1A, NW1 and E1") # => []
274
+ ```
275
+
276
+ `replace()` replaces postcode-shaped values and returns a `ReplaceResult`
277
+ containing the matches and resulting text:
278
+
279
+ ```python
280
+ from postcode import replace
281
+
282
+ replacement = replace("The two addresses are SW1A2AA and SW1A 2AB")
283
+ replacement.match
284
+ # => ["SW1A2AA", "SW1A 2AB"]
285
+ replacement.result
286
+ # => "The two addresses are and "
287
+
288
+ replace("The address is SW1A 2AA", "Downing Street").result
289
+ # => "The address is Downing Street"
290
+ ```
291
+
292
+ ### Regular expressions
293
+
294
+ The compiled regular expressions used by the package are public:
295
+
296
+ ```python
297
+ from postcode import POSTCODE_REGEX
298
+
299
+ bool(POSTCODE_REGEX.match("SW1A 2AA")) # => True
300
+ ```
301
+
302
+ The other exports are `AREA_REGEX`, `DISTRICT_SPLIT_REGEX`, `FIXABLE_REGEX`,
303
+ `INCODE_REGEX`, `OUTCODE_REGEX`, `POSTCODE_CORPUS_REGEX`, and `UNIT_REGEX`.
304
+
305
+ ## Definitions
306
+
307
+ A UK postcode is made up of an outward code and an inward code. The outward
308
+ code contains the area and district, and can include a sub-district. The inward
309
+ code contains the sector digit and unit letters. For example, in `SW1A 2AA`:
310
+
311
+ - Area: `SW`
312
+ - District: `SW1`
313
+ - Sub-district: `SW1A`
314
+ - Outcode: `SW1A`
315
+ - Incode: `2AA`
316
+ - Sector: `SW1A 2`
317
+ - Unit: `AA`
318
+
319
+ ## Notes
320
+
321
+ Postcodes cannot be authoritatively validated with a regular expression,
322
+ however complex. True validation requires checking against a current postcode
323
+ dataset. This package validates the *shape* of a postcode and can therefore
324
+ produce false positives or negatives when used as an existence check.
325
+
326
+ ## Development
327
+
328
+ ```bash
329
+ python -m pip install -e ".[dev]"
330
+ pytest
331
+ ruff check .
332
+ mypy
333
+ python -m build
334
+ ```
335
+
336
+ ## License
337
+
338
+ MIT
339
+
340
+ Contains Ordnance Survey Data © Crown Copyright & Database Right.
@@ -0,0 +1,307 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/area360-uk/py-postcode/main/assets/py-postcode.png" alt="Py UK Postcode" width="100%">
3
+ </p>
4
+
5
+ # Py UK Postcode
6
+
7
+ > Validate and parse UK postcodes in Python
8
+
9
+ [![CI](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml/badge.svg)](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml)
10
+ [![PyPI](https://img.shields.io/pypi/v/py-uk-postcode.svg)](https://pypi.org/project/py-uk-postcode/)
11
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
12
+
13
+ Utility methods for UK postcodes, including validating the shape of a postcode
14
+ and extracting postcode elements such as incodes, outcodes, areas, and
15
+ [more](#definitions).
16
+
17
+ The implementation is a close Python port of [ideal-postcodes/postcode](https://github.com/ideal-postcodes/postcode), whose format rules
18
+ were tested against roughly 1.7 million postcodes from the ONS Postcode
19
+ Directory.
20
+
21
+ ## Features
22
+
23
+ - [Check](#validate) whether a postcode conforms to the
24
+ [correct format](https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#formatting)
25
+ - Small, single-purpose functions with no runtime dependencies
26
+ - [Extract](#parse) postcode elements such as the incode, outcode, and sector
27
+ - Search for and replace postcodes in larger bodies of text
28
+ - Correct common `O`/`0` and `I`/`1` input mistakes
29
+ - Fully typed Python API
30
+
31
+ ## Acknowledgments and credits
32
+
33
+ `py-uk-postcode` is a Python port of the original [ideal-postcodes/postcode](https://github.com/ideal-postcodes/postcode), created
34
+ by Ideal Postcodes and released under the MIT License. The original project's
35
+ API design, implementation, documentation, and test fixtures formed the basis
36
+ of this port.
37
+
38
+ ## Links
39
+
40
+ - [GitHub repository](https://github.com/area360-uk/py-postcode)
41
+ - [Package on PyPI](https://pypi.org/project/py-uk-postcode/)
42
+ - [Issue tracker](https://github.com/area360-uk/py-postcode/issues)
43
+ - [Postcode element definitions](#definitions)
44
+ - [Notes](#notes)
45
+
46
+ ## Getting started
47
+
48
+ ### Installation
49
+
50
+ With `pip`:
51
+
52
+ ```bash
53
+ python -m pip install py-uk-postcode
54
+ ```
55
+
56
+ With `uv`:
57
+
58
+ ```bash
59
+ uv add py-uk-postcode
60
+ ```
61
+
62
+ With Poetry:
63
+
64
+ ```bash
65
+ poetry add py-uk-postcode
66
+ ```
67
+
68
+ With PDM:
69
+
70
+ ```bash
71
+ pdm add py-uk-postcode
72
+ ```
73
+
74
+ With Conda, create or activate an environment and install the package from
75
+ PyPI:
76
+
77
+ ```bash
78
+ conda create --name postcode python=3.14 pip
79
+ conda activate postcode
80
+ python -m pip install py-uk-postcode
81
+ ```
82
+
83
+ The distribution is named `py-uk-postcode`; import it in Python as `postcode`.
84
+
85
+ ### Validate
86
+
87
+ ```python
88
+ from postcode import is_valid
89
+
90
+ is_valid("AA1 1AB") # => True
91
+ ```
92
+
93
+ ### Parse
94
+
95
+ Pass a string to `parse()`. It returns either a `ValidPostcode` or an
96
+ `InvalidPostcode`, both of which expose their values as attributes.
97
+
98
+ #### Valid postcode
99
+
100
+ ```python
101
+ from postcode import parse
102
+
103
+ result = parse("Sw1A 2aa")
104
+
105
+ result.postcode # => "SW1A 2AA"
106
+ result.outcode # => "SW1A"
107
+ result.incode # => "2AA"
108
+ result.area # => "SW"
109
+ result.district # => "SW1"
110
+ result.unit # => "AA"
111
+ result.sector # => "SW1A 2"
112
+ result.sub_district # => "SW1A"
113
+ result.valid # => True
114
+ ```
115
+
116
+ #### Invalid postcode
117
+
118
+ ```python
119
+ result = parse(" Oh no, ): ")
120
+
121
+ result.postcode # => None
122
+ result.outcode # => None
123
+ result.incode # => None
124
+ result.area # => None
125
+ result.district # => None
126
+ result.unit # => None
127
+ result.sector # => None
128
+ result.sub_district # => None
129
+ result.valid # => False
130
+ ```
131
+
132
+ #### Type narrowing
133
+
134
+ `parse()` returns a typed union. Type checkers can narrow it by checking the
135
+ literal `valid` attribute:
136
+
137
+ ```python
138
+ from postcode import parse
139
+
140
+ result = parse("SW1A 2AA")
141
+
142
+ if result.valid:
143
+ print(result.outcode.lower())
144
+ if result.sub_district is not None:
145
+ print(result.sub_district.lower())
146
+ else:
147
+ print("Invalid postcode")
148
+ ```
149
+
150
+ #### Valid postcode object
151
+
152
+ | Postcode | `.outcode` | `.incode` | `.area` | `.district` | `.sub_district` | `.sector` | `.unit` |
153
+ |----------|------------|-----------|---------|-------------|-----------------|-----------|---------|
154
+ | AA9A 9AA | AA9A | 9AA | AA | AA9 | AA9A | AA9A 9 | AA |
155
+ | A9A 9AA | A9A | 9AA | A | A9 | A9A | A9A 9 | AA |
156
+ | A9 9AA | A9 | 9AA | A | A9 | `None` | A9 9 | AA |
157
+ | A99 9AA | A99 | 9AA | A | A99 | `None` | A99 9 | AA |
158
+ | AA9 9AA | AA9 | 9AA | AA | AA9 | `None` | AA9 9 | AA |
159
+ | AA99 9AA | AA99 | 9AA | AA | AA99 | `None` | AA99 9 | AA |
160
+
161
+ ### Exported functions
162
+
163
+ If you need a single value, import the corresponding function directly.
164
+
165
+ #### Validation
166
+
167
+ ```python
168
+ from postcode import is_valid, valid_outcode
169
+
170
+ is_valid("Sw1A 2aa") # => True
171
+ valid_outcode("SW1A") # => True
172
+ ```
173
+
174
+ #### Formatting
175
+
176
+ ```python
177
+ from postcode import (
178
+ to_area,
179
+ to_district,
180
+ to_incode,
181
+ to_normalised,
182
+ to_outcode,
183
+ to_sector,
184
+ to_sub_district,
185
+ to_unit,
186
+ )
187
+
188
+ to_normalised("Sw1A 2aa") # => "SW1A 2AA"
189
+ to_outcode("Sw1A 2aa") # => "SW1A"
190
+ to_incode("Sw1A 2aa") # => "2AA"
191
+ to_area("Sw1A 2aa") # => "SW"
192
+ to_district("Sw1A 2aa") # => "SW1"
193
+ to_sub_district("Sw1A 2aa") # => "SW1A"
194
+ to_sector("Sw1A 2aa") # => "SW1A 2"
195
+ to_unit("Sw1A 2aa") # => "AA"
196
+ ```
197
+
198
+ All formatting functions return `None` when given an invalid postcode.
199
+
200
+ #### Fix
201
+
202
+ `fix()` attempts to clean up a postcode without validating it. It replaces
203
+ commonly confused characters (`O`/`0` and `I`/`1`), uppercases the value, and
204
+ corrects its spacing. If the input cannot be reliably fixed, the original
205
+ string is returned.
206
+
207
+ ```python
208
+ from postcode import fix, parse
209
+
210
+ fix("SWIA 2AA") # => "SW1A 2AA"
211
+ fix("SW1A 21A") # => "SW1A 2IA"
212
+ fix("SW1A OAA") # => "SW1A 0AA"
213
+ fix("SW1A 20A") # => "SW1A 2OA"
214
+ fix(" SW1A 2AO") # => "SW1A 2AO"
215
+ fix("sw1a 2aa") # => "SW1A 2AA"
216
+
217
+ result = parse(fix("SW1A 2A0"))
218
+ result.incode # => "2AO"
219
+
220
+ fix("12a") # => "12a"
221
+ ```
222
+
223
+ #### Extract and replace
224
+
225
+ `match()` retrieves postcode-shaped values from a body of text. Matches retain
226
+ their original casing and spacing.
227
+
228
+ ```python
229
+ from postcode import match, to_normalised, to_outcode
230
+
231
+ matches = match("The two addresses are SW1A2aa and SW1A 2AB")
232
+ # => ["SW1A2aa", "SW1A 2AB"]
233
+
234
+ [to_normalised(value) for value in matches]
235
+ # => ["SW1A 2AA", "SW1A 2AB"]
236
+
237
+ [to_outcode(value) for value in matches]
238
+ # => ["SW1A", "SW1A"]
239
+
240
+ match("Some London outward codes are SW1A, NW1 and E1") # => []
241
+ ```
242
+
243
+ `replace()` replaces postcode-shaped values and returns a `ReplaceResult`
244
+ containing the matches and resulting text:
245
+
246
+ ```python
247
+ from postcode import replace
248
+
249
+ replacement = replace("The two addresses are SW1A2AA and SW1A 2AB")
250
+ replacement.match
251
+ # => ["SW1A2AA", "SW1A 2AB"]
252
+ replacement.result
253
+ # => "The two addresses are and "
254
+
255
+ replace("The address is SW1A 2AA", "Downing Street").result
256
+ # => "The address is Downing Street"
257
+ ```
258
+
259
+ ### Regular expressions
260
+
261
+ The compiled regular expressions used by the package are public:
262
+
263
+ ```python
264
+ from postcode import POSTCODE_REGEX
265
+
266
+ bool(POSTCODE_REGEX.match("SW1A 2AA")) # => True
267
+ ```
268
+
269
+ The other exports are `AREA_REGEX`, `DISTRICT_SPLIT_REGEX`, `FIXABLE_REGEX`,
270
+ `INCODE_REGEX`, `OUTCODE_REGEX`, `POSTCODE_CORPUS_REGEX`, and `UNIT_REGEX`.
271
+
272
+ ## Definitions
273
+
274
+ A UK postcode is made up of an outward code and an inward code. The outward
275
+ code contains the area and district, and can include a sub-district. The inward
276
+ code contains the sector digit and unit letters. For example, in `SW1A 2AA`:
277
+
278
+ - Area: `SW`
279
+ - District: `SW1`
280
+ - Sub-district: `SW1A`
281
+ - Outcode: `SW1A`
282
+ - Incode: `2AA`
283
+ - Sector: `SW1A 2`
284
+ - Unit: `AA`
285
+
286
+ ## Notes
287
+
288
+ Postcodes cannot be authoritatively validated with a regular expression,
289
+ however complex. True validation requires checking against a current postcode
290
+ dataset. This package validates the *shape* of a postcode and can therefore
291
+ produce false positives or negatives when used as an existence check.
292
+
293
+ ## Development
294
+
295
+ ```bash
296
+ python -m pip install -e ".[dev]"
297
+ pytest
298
+ ruff check .
299
+ mypy
300
+ python -m build
301
+ ```
302
+
303
+ ## License
304
+
305
+ MIT
306
+
307
+ Contains Ordnance Survey Data © Crown Copyright & Database Right.
@@ -0,0 +1,11 @@
1
+ from postcode import is_valid, parse, to_outcode
2
+
3
+ # Quickly sanity-check a postcode.
4
+ print(is_valid("SW1A 2AA"))
5
+
6
+ # Extract the outward code.
7
+ print(to_outcode("SW1A 2AA"))
8
+
9
+ # Parse a postcode to access its different parts.
10
+ postcode = parse("Sw1a 2AA")
11
+ print(postcode)