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.
- py_uk_postcode-1.0.0/CHANGELOG.md +13 -0
- py_uk_postcode-1.0.0/LICENSE +22 -0
- py_uk_postcode-1.0.0/MANIFEST.in +4 -0
- py_uk_postcode-1.0.0/PKG-INFO +340 -0
- py_uk_postcode-1.0.0/README.md +307 -0
- py_uk_postcode-1.0.0/assets/py-postcode.png +0 -0
- py_uk_postcode-1.0.0/example.py +11 -0
- py_uk_postcode-1.0.0/pyproject.toml +76 -0
- py_uk_postcode-1.0.0/setup.cfg +4 -0
- py_uk_postcode-1.0.0/src/postcode/__init__.py +365 -0
- py_uk_postcode-1.0.0/src/postcode/py.typed +1 -0
- py_uk_postcode-1.0.0/src/py_uk_postcode.egg-info/PKG-INFO +340 -0
- py_uk_postcode-1.0.0/src/py_uk_postcode.egg-info/SOURCES.txt +24 -0
- py_uk_postcode-1.0.0/src/py_uk_postcode.egg-info/dependency_links.txt +1 -0
- py_uk_postcode-1.0.0/src/py_uk_postcode.egg-info/requires.txt +7 -0
- py_uk_postcode-1.0.0/src/py_uk_postcode.egg-info/top_level.txt +1 -0
- py_uk_postcode-1.0.0/tests/data/areas.json +100 -0
- py_uk_postcode-1.0.0/tests/data/districts.json +120 -0
- py_uk_postcode-1.0.0/tests/data/incodes.json +100 -0
- py_uk_postcode-1.0.0/tests/data/normalisation.json +56 -0
- py_uk_postcode-1.0.0/tests/data/outcodes.json +100 -0
- py_uk_postcode-1.0.0/tests/data/sectors.json +100 -0
- py_uk_postcode-1.0.0/tests/data/sub-districts.json +92 -0
- py_uk_postcode-1.0.0/tests/data/units.json +100 -0
- py_uk_postcode-1.0.0/tests/data/validation.json +56 -0
- py_uk_postcode-1.0.0/tests/test_postcode.py +223 -0
|
@@ -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,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
|
+
[](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/py-uk-postcode/)
|
|
44
|
+
[](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
|
+
[](https://github.com/area360-uk/py-postcode/actions/workflows/ci.yml)
|
|
10
|
+
[](https://pypi.org/project/py-uk-postcode/)
|
|
11
|
+
[](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.
|
|
Binary file
|
|
@@ -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)
|